feat: plugin-import-export initial work (#10795)
Adds new plugin-import-export initial version.
Allows for direct download and creation of downloadable collection data
stored to a json or csv uses the access control of the user creating the
request to make the file.
config options:
```ts
/**
* Collections to include the Import/Export controls in
* Defaults to all collections
*/
collections?: string[]
/**
* Enable to force the export to run synchronously
*/
disableJobsQueue?: boolean
/**
* This function takes the default export collection configured in the plugin and allows you to override it by modifying and returning it
* @param collection
* @returns collection
*/
overrideExportCollection?: (collection: CollectionOverride) => CollectionOverride
// payload.config.ts:
plugins: [
importExportPlugin({
collections: ['pages', 'users'],
overrideExportCollection: (collection) => {
collection.admin.group = 'System'
collection.upload.staticDir = path.resolve(dirname, 'uploads')
return collection
},
disableJobsQueue: true,
}),
],
```
---------
Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
Co-authored-by: Kendell Joseph <kendelljoseph@gmail.com>
This commit is contained in:
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -313,6 +313,7 @@ jobs:
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-import-export
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- versions
|
||||
|
||||
1
.idea/payload.iml
generated
1
.idea/payload.iml
generated
@@ -80,6 +80,7 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-import-export/dist" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"build:payload-cloud": "turbo build --filter \"@payloadcms/payload-cloud\"",
|
||||
"build:plugin-cloud-storage": "turbo build --filter \"@payloadcms/plugin-cloud-storage\"",
|
||||
"build:plugin-form-builder": "turbo build --filter \"@payloadcms/plugin-form-builder\"",
|
||||
"build:plugin-import-export": "turbo build --filter \"@payloadcms/plugin-import-export\"",
|
||||
"build:plugin-multi-tenant": "turbo build --filter \"@payloadcms/plugin-multi-tenant\"",
|
||||
"build:plugin-nested-docs": "turbo build --filter \"@payloadcms/plugin-nested-docs\"",
|
||||
"build:plugin-redirects": "turbo build --filter \"@payloadcms/plugin-redirects\"",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const PAYLOAD_PACKAGE_LIST = [
|
||||
'@payloadcms/plugin-cloud-storage',
|
||||
'@payloadcms/payload-cloud',
|
||||
'@payloadcms/plugin-form-builder',
|
||||
'@payloadcms/plugin-import-export',
|
||||
// '@payloadcms/plugin-multi-tenant',
|
||||
'@payloadcms/plugin-nested-docs',
|
||||
'@payloadcms/plugin-redirects',
|
||||
|
||||
7
packages/plugin-import-export/.gitignore
vendored
Normal file
7
packages/plugin-import-export/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.env
|
||||
dist
|
||||
demo/uploads
|
||||
build
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
12
packages/plugin-import-export/.prettierignore
Normal file
12
packages/plugin-import-export/.prettierignore
Normal file
@@ -0,0 +1,12 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
**/docs/**
|
||||
tsconfig.json
|
||||
24
packages/plugin-import-export/.swcrc
Normal file
24
packages/plugin-import-export/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
7
packages/plugin-import-export/README.md
Normal file
7
packages/plugin-import-export/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Payload Import/Export Plugin
|
||||
|
||||
A plugin for [Payload](https://github.com/payloadcms/payload) to easily import and export data.
|
||||
|
||||
- [Source code](https://github.com/payloadcms/payload/tree/main/packages/plugin-import-export)
|
||||
- [Documentation](https://payloadcms.com/docs/plugins/import-export)
|
||||
- [Documentation source](https://github.com/payloadcms/payload/tree/main/docs/plugins/import-export.mdx)
|
||||
18
packages/plugin-import-export/eslint.config.js
Normal file
18
packages/plugin-import-export/eslint.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
22
packages/plugin-import-export/license.md
Normal file
22
packages/plugin-import-export/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
101
packages/plugin-import-export/package.json
Normal file
101
packages/plugin-import-export/package.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.26.0",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
"cms",
|
||||
"plugin",
|
||||
"typescript",
|
||||
"react",
|
||||
"nextjs",
|
||||
"import",
|
||||
"export"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/payloadcms/payload.git",
|
||||
"directory": "packages/plugin-import-export"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/exports/types.ts",
|
||||
"types": "./src/exports/types.ts",
|
||||
"default": "./src/exports/types.ts"
|
||||
},
|
||||
"./rsc": {
|
||||
"import": "./src/exports/rsc.ts",
|
||||
"types": "./src/exports/rsc.ts",
|
||||
"default": "./src/exports/rsc.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faceless-ui/modal": "3.0.0-beta.2",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-stringify": "^6.5.2",
|
||||
"qs-esm": "7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/exports/types.js",
|
||||
"types": "./dist/exports/types.d.ts",
|
||||
"default": "./dist/exports/types.js"
|
||||
},
|
||||
"./rsc": {
|
||||
"import": "./dist/exports/rsc.js",
|
||||
"types": "./dist/exports/rsc.d.ts",
|
||||
"default": "./dist/exports/rsc.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"homepage:": "https://payloadcms.com"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import type React from 'react'
|
||||
|
||||
import { useDocumentInfo, useField } from '@payloadcms/ui'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
|
||||
export const CollectionField: React.FC = () => {
|
||||
const { id } = useDocumentInfo()
|
||||
const { setValue } = useField({ path: 'collectionSlug' })
|
||||
const { collection } = useImportExport()
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
return
|
||||
}
|
||||
setValue(collection)
|
||||
}, [id, collection, setValue])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
@layer payload-default {
|
||||
.export-list-menu-item {
|
||||
.doc-drawer__toggler {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
// TODO: is any of this css needed?
|
||||
&__subheader,
|
||||
&__header {
|
||||
padding: 0 var(--gutter-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--theme-border-color);
|
||||
|
||||
& h2 {
|
||||
margin: calc(var(--gutter-h) * 0.5) 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__options,
|
||||
&__preview {
|
||||
padding: calc(var(--gutter-h) * 0.5) var(--gutter-h);
|
||||
}
|
||||
|
||||
&__preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: calc(var(--gutter-h) * 0.5);
|
||||
}
|
||||
|
||||
&__close {
|
||||
@include btn-reset;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { PopupList, useConfig, useDocumentDrawer, useTranslation } from '@payloadcms/ui'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'export-list-menu-item'
|
||||
|
||||
export const ExportListMenuItem: React.FC<{
|
||||
collectionSlug: string
|
||||
exportCollectionSlug: string
|
||||
}> = ({ collectionSlug, exportCollectionSlug }) => {
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { i18n } = useTranslation()
|
||||
const currentCollectionConfig = getEntityConfig({ collectionSlug })
|
||||
|
||||
const [DocumentDrawer, DocumentDrawerToggler] = useDocumentDrawer({
|
||||
collectionSlug: exportCollectionSlug,
|
||||
})
|
||||
const { setCollection } = useImportExport()
|
||||
|
||||
// Set collection and selected items on mount or when selection changes
|
||||
useEffect(() => {
|
||||
setCollection(currentCollectionConfig.slug ?? '')
|
||||
}, [currentCollectionConfig, setCollection])
|
||||
|
||||
return (
|
||||
<PopupList.Button className={baseClass}>
|
||||
<DocumentDrawerToggler>
|
||||
Export {getTranslation(currentCollectionConfig.labels.plural, i18n)}
|
||||
</DocumentDrawerToggler>
|
||||
<DocumentDrawer />
|
||||
</PopupList.Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import { Button, SaveButton, useConfig, useForm, useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const ExportSaveButton: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
config: {
|
||||
routes: { api },
|
||||
serverURL,
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
const { getData } = useForm()
|
||||
|
||||
const label = t('general:save')
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const data = getData()
|
||||
const response = await fetch(`${serverURL}${api}/exports/download`, {
|
||||
body: JSON.stringify({
|
||||
data,
|
||||
}),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to download file')
|
||||
}
|
||||
|
||||
const fileStream = response.body
|
||||
const reader = fileStream?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let result = ''
|
||||
|
||||
while (reader) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
result += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
const blob = new Blob([result], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${data.name}.${data.format}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SaveButton label={label}></SaveButton>
|
||||
<Button onClick={handleDownload} size="medium" type="button">
|
||||
Download
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import type { ListPreferences, SelectFieldClientComponent } from 'payload'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
FieldLabel,
|
||||
ReactSelect,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useField,
|
||||
usePreferences,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
import { reduceFields } from './reduceFields.js'
|
||||
|
||||
const baseClass = 'fields-to-export'
|
||||
|
||||
export const FieldsToExport: SelectFieldClientComponent = (props) => {
|
||||
const { id } = useDocumentInfo()
|
||||
const { path } = props
|
||||
const { setValue, value } = useField<string[]>({ path })
|
||||
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { collection } = useImportExport()
|
||||
const { getPreference } = usePreferences()
|
||||
const [displayedValue, setDisplayedValue] = useState<
|
||||
{ id: string; label: ReactNode; value: string }[]
|
||||
>([])
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: collectionSlug ?? collection })
|
||||
const fieldOptions = reduceFields({ fields: collectionConfig?.fields })
|
||||
|
||||
useEffect(() => {
|
||||
if (value && value.length > 0) {
|
||||
setDisplayedValue((prevDisplayedValue) => {
|
||||
if (prevDisplayedValue.length > 0) {
|
||||
return prevDisplayedValue
|
||||
} // Prevent unnecessary updates
|
||||
|
||||
return value.map((field) => {
|
||||
const match = fieldOptions.find((option) => option.value === field)
|
||||
return match ? { ...match, id: field } : { id: field, label: field, value: field }
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [value, fieldOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (id || !collectionSlug) {
|
||||
return
|
||||
}
|
||||
const doAsync = async () => {
|
||||
const currentPreferences = await getPreference<{
|
||||
columns: ListPreferences['columns']
|
||||
}>(`${collectionSlug}-list`)
|
||||
|
||||
const columns = currentPreferences?.columns?.filter((a) => a.active).map((b) => b.accessor)
|
||||
setValue(columns ?? collectionConfig?.admin?.defaultColumns ?? [])
|
||||
}
|
||||
|
||||
void doAsync()
|
||||
}, [
|
||||
getPreference,
|
||||
collection,
|
||||
setValue,
|
||||
collectionSlug,
|
||||
id,
|
||||
collectionConfig?.admin?.defaultColumns,
|
||||
])
|
||||
const onChange = (options: { id: string; label: ReactNode; value: string }[]) => {
|
||||
if (!options) {
|
||||
setValue([])
|
||||
return
|
||||
}
|
||||
const updatedValue = options?.map((option) =>
|
||||
typeof option === 'object' ? option.value : option,
|
||||
)
|
||||
setValue(updatedValue)
|
||||
setDisplayedValue(options)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<FieldLabel label="Columns to Export" />
|
||||
<ReactSelect
|
||||
className={baseClass}
|
||||
disabled={props.readOnly}
|
||||
getOptionValue={(option) => String(option.value)}
|
||||
isClearable={true}
|
||||
isMulti={true}
|
||||
isSortable={true}
|
||||
// @ts-expect-error react select option
|
||||
onChange={onChange}
|
||||
options={fieldOptions}
|
||||
value={displayedValue}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldHasSubFields } from 'payload/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
const createNestedClientFieldPath = (parentPath: string, field: ClientField): string => {
|
||||
if (parentPath) {
|
||||
if (fieldAffectsData(field)) {
|
||||
return `${parentPath}.${field.name}`
|
||||
}
|
||||
return parentPath
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
return field.name
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const combineLabel = ({
|
||||
field,
|
||||
prefix,
|
||||
}: {
|
||||
field: ClientField
|
||||
prefix?: React.ReactNode
|
||||
}): React.ReactNode => {
|
||||
return (
|
||||
<Fragment>
|
||||
{prefix ? (
|
||||
<Fragment>
|
||||
<span style={{ display: 'inline-block' }}>{prefix}</span>
|
||||
{' > '}
|
||||
</Fragment>
|
||||
) : null}
|
||||
<span style={{ display: 'inline-block' }}>
|
||||
{'label' in field && typeof field.label === 'string'
|
||||
? field.label
|
||||
: (('name' in field && field.name) ?? 'unnamed field')}
|
||||
</span>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export const reduceFields = ({
|
||||
fields,
|
||||
labelPrefix = null,
|
||||
path = '',
|
||||
}: {
|
||||
fields: ClientField[]
|
||||
labelPrefix?: React.ReactNode
|
||||
path?: string
|
||||
}): { id: string; label: React.ReactNode; value: string }[] => {
|
||||
if (!fields) {
|
||||
return []
|
||||
}
|
||||
|
||||
return fields.reduce<{ id: string; label: React.ReactNode; value: string }[]>(
|
||||
(fieldsToUse, field) => {
|
||||
// escape for a variety of reasons, include ui fields as they have `name`.
|
||||
if (field.type === 'ui') {
|
||||
return fieldsToUse
|
||||
}
|
||||
|
||||
if (!(field.type === 'array' || field.type === 'blocks') && fieldHasSubFields(field)) {
|
||||
return [
|
||||
...fieldsToUse,
|
||||
...reduceFields({
|
||||
fields: field.fields,
|
||||
labelPrefix: combineLabel({ field, prefix: labelPrefix }),
|
||||
path: createNestedClientFieldPath(path, field),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
if (field.type === 'tabs' && 'tabs' in field) {
|
||||
return [
|
||||
...fieldsToUse,
|
||||
...field.tabs.reduce<{ id: string; label: React.ReactNode; value: string }[]>(
|
||||
(tabFields, tab) => {
|
||||
if ('fields' in tab) {
|
||||
const isNamedTab = 'name' in tab && tab.name
|
||||
return [
|
||||
...tabFields,
|
||||
...reduceFields({
|
||||
fields: tab.fields,
|
||||
labelPrefix,
|
||||
path: isNamedTab ? createNestedClientFieldPath(path, field) : path,
|
||||
}),
|
||||
]
|
||||
}
|
||||
return tabFields
|
||||
},
|
||||
[],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
const val = createNestedClientFieldPath(path, field)
|
||||
|
||||
const formattedField = {
|
||||
id: val,
|
||||
label: combineLabel({ field, prefix: labelPrefix }),
|
||||
value: val,
|
||||
}
|
||||
|
||||
return [...fieldsToUse, formattedField]
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import React, { createContext, useCallback, useContext, useState } from 'react'
|
||||
|
||||
type ImportExportContext = {
|
||||
collection: string
|
||||
setCollection: (collection: string) => void
|
||||
}
|
||||
|
||||
export const ImportExportContext = createContext({} as ImportExportContext)
|
||||
|
||||
export const ImportExportProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [collection, setCollectionState] = useState<string>('')
|
||||
|
||||
const setCollection = useCallback((collection: string) => {
|
||||
setCollectionState(collection)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ImportExportContext.Provider
|
||||
value={{
|
||||
collection,
|
||||
setCollection,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ImportExportContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useImportExport = (): ImportExportContext => useContext(ImportExportContext)
|
||||
@@ -0,0 +1,8 @@
|
||||
.preview {
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
109
packages/plugin-import-export/src/components/Preview/index.tsx
Normal file
109
packages/plugin-import-export/src/components/Preview/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
import type { Column } from '@payloadcms/ui'
|
||||
import type { ClientField, FieldAffectingDataClient } from 'payload'
|
||||
|
||||
import { Table, useConfig, useField } from '@payloadcms/ui'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import * as qs from 'qs-esm'
|
||||
import React from 'react'
|
||||
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'preview'
|
||||
|
||||
export const Preview = () => {
|
||||
const { collection } = useImportExport()
|
||||
const { config } = useConfig()
|
||||
const { value: where } = useField({ path: 'where' })
|
||||
const { value: limit } = useField<number>({ path: 'limit' })
|
||||
const { value: fields } = useField<string[]>({ path: 'fields' })
|
||||
const { value: sort } = useField({ path: 'sort' })
|
||||
const { value: draft } = useField({ path: 'draft' })
|
||||
const [dataToRender, setDataToRender] = React.useState<any[]>([])
|
||||
const [resultCount, setResultCount] = React.useState<any>('')
|
||||
const [columns, setColumns] = React.useState<Column[]>([])
|
||||
|
||||
const collectionSlug = typeof collection === 'string' && collection
|
||||
const collectionConfig = config.collections.find(
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!collectionSlug) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const whereQuery = qs.stringify(
|
||||
{
|
||||
depth: 0,
|
||||
draft,
|
||||
limit: limit > 10 ? 10 : limit,
|
||||
sort,
|
||||
where,
|
||||
},
|
||||
{
|
||||
addQueryPrefix: true,
|
||||
},
|
||||
)
|
||||
const response = await fetch(`/api/${collectionSlug}${whereQuery}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setResultCount(limit && limit < data.totalDocs ? limit : data.totalDocs)
|
||||
// TODO: check if this data is in the correct format for the table
|
||||
|
||||
const filteredFields = (collectionConfig?.fields?.filter((field) => {
|
||||
if (!fieldAffectsData(field)) {
|
||||
return false
|
||||
}
|
||||
if (fields?.length > 0) {
|
||||
return fields.includes(field.name)
|
||||
}
|
||||
return true
|
||||
}) ?? []) as FieldAffectingDataClient[]
|
||||
|
||||
setColumns(
|
||||
filteredFields.map((field) => ({
|
||||
accessor: field.name || '',
|
||||
active: true,
|
||||
field: field as ClientField,
|
||||
Heading: field?.label || field.name,
|
||||
renderedCells: data.docs.map((doc: Record<string, unknown>) => {
|
||||
if (!field.name || !doc[field.name]) {
|
||||
return null
|
||||
}
|
||||
if (typeof doc[field.name] === 'object') {
|
||||
return JSON.stringify(doc[field.name])
|
||||
}
|
||||
return String(doc[field.name])
|
||||
}),
|
||||
})) as Column[],
|
||||
)
|
||||
setDataToRender(data.docs)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
void fetchData()
|
||||
}, [collectionConfig?.fields, collectionSlug, draft, fields, limit, sort, where])
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<h3>Preview</h3>
|
||||
{resultCount && <span>{resultCount} total documents</span>}
|
||||
</div>
|
||||
{dataToRender && <Table columns={columns} data={dataToRender} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.sort-by-fields {
|
||||
display: block;
|
||||
width: 33%;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'use client'
|
||||
|
||||
import type { SelectFieldClientComponent } from 'payload'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import {
|
||||
FieldLabel,
|
||||
ReactSelect,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useField,
|
||||
useListQuery,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { reduceFields } from '../FieldsToExport/reduceFields.js'
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
|
||||
const baseClass = 'sort-by-fields'
|
||||
|
||||
export const SortBy: SelectFieldClientComponent = (props) => {
|
||||
const { id } = useDocumentInfo()
|
||||
const { path } = props
|
||||
const { setValue, value } = useField<string>({ path })
|
||||
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
|
||||
const { query } = useListQuery()
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { collection } = useImportExport()
|
||||
const [displayedValue, setDisplayedValue] = useState<{
|
||||
id: string
|
||||
label: ReactNode
|
||||
value: string
|
||||
} | null>(null)
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: collectionSlug ?? collection })
|
||||
const fieldOptions = reduceFields({ fields: collectionConfig?.fields })
|
||||
|
||||
// Sync displayedValue with value from useField
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setDisplayedValue(null)
|
||||
return
|
||||
}
|
||||
|
||||
const option = fieldOptions.find((field) => field.value === value)
|
||||
if (option && (!displayedValue || displayedValue.value !== value)) {
|
||||
setDisplayedValue(option)
|
||||
}
|
||||
}, [value, fieldOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (id || !query?.sort || value) {
|
||||
return
|
||||
}
|
||||
|
||||
const option = fieldOptions.find((field) => field.value === query.sort)
|
||||
if (option) {
|
||||
setValue(option.value)
|
||||
setDisplayedValue(option)
|
||||
}
|
||||
}, [fieldOptions, id, query?.sort, value, setValue])
|
||||
|
||||
const onChange = (option: { id: string; label: ReactNode; value: string } | null) => {
|
||||
if (!option) {
|
||||
setValue('')
|
||||
setDisplayedValue(null)
|
||||
} else {
|
||||
setValue(option.value)
|
||||
setDisplayedValue(option)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass} style={{ '--field-width': '33%' } as React.CSSProperties}>
|
||||
<FieldLabel label="Sort By" />
|
||||
<ReactSelect
|
||||
className={baseClass}
|
||||
disabled={props.readOnly}
|
||||
getOptionValue={(option) => String(option.value)}
|
||||
isClearable={true}
|
||||
isSortable={true}
|
||||
// @ts-expect-error react select option
|
||||
onChange={onChange}
|
||||
options={fieldOptions}
|
||||
// @ts-expect-error react select
|
||||
value={displayedValue}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
|
||||
import { useDocumentInfo, useField, useListQuery, useSelection } from '@payloadcms/ui'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const WhereField: React.FC = () => {
|
||||
const { setValue: setSelectionToUseValue, value: selectionToUseValue } = useField({
|
||||
path: 'selectionToUse',
|
||||
})
|
||||
const { setValue } = useField({ path: 'where' })
|
||||
const { selectAll, selected } = useSelection()
|
||||
const { query } = useListQuery()
|
||||
const { id } = useDocumentInfo()
|
||||
|
||||
// setValue based on selectionToUseValue
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectionToUseValue === 'currentFilters' && query && query?.where) {
|
||||
setValue(query.where)
|
||||
}
|
||||
|
||||
if (selectionToUseValue === 'currentSelection' && selected) {
|
||||
const ids = []
|
||||
|
||||
for (const [key, value] of selected) {
|
||||
if (value) {
|
||||
ids.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
setValue({
|
||||
id: {
|
||||
in: ids,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (selectionToUseValue === 'all' && selected) {
|
||||
setValue({})
|
||||
}
|
||||
|
||||
// Selected set a where query with IDs
|
||||
}, [id, selectionToUseValue, query, selected, setValue])
|
||||
|
||||
// handles default value of selectionToUse
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
return
|
||||
}
|
||||
let defaultSelection: 'all' | 'currentFilters' | 'currentSelection' = 'all'
|
||||
|
||||
if (['allInPage', 'some'].includes(selectAll)) {
|
||||
defaultSelection = 'currentSelection'
|
||||
}
|
||||
|
||||
if (defaultSelection === 'all' && query?.where) {
|
||||
defaultSelection = 'currentFilters'
|
||||
}
|
||||
|
||||
setSelectionToUseValue(defaultSelection)
|
||||
}, [id, query, selectAll, setSelectionToUseValue])
|
||||
|
||||
return null
|
||||
}
|
||||
151
packages/plugin-import-export/src/export/createExport.ts
Normal file
151
packages/plugin-import-export/src/export/createExport.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { PaginatedDocs, PayloadRequest, Sort, User, Where } from 'payload'
|
||||
|
||||
import { stringify } from 'csv-stringify/sync'
|
||||
import { APIError } from 'payload'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
import { flattenObject } from './flattenObject.js'
|
||||
import { getFilename } from './getFilename.js'
|
||||
import { getSelect } from './getSelect.js'
|
||||
|
||||
type Export = {
|
||||
collectionSlug: string
|
||||
exportsCollection: string
|
||||
fields?: string[]
|
||||
format: 'csv' | 'json'
|
||||
globals?: string[]
|
||||
id: number | string
|
||||
locale?: string
|
||||
name: string
|
||||
slug: string
|
||||
sort: Sort
|
||||
user: string
|
||||
userCollection: string
|
||||
where?: Where
|
||||
}
|
||||
|
||||
export type CreateExportArgs = {
|
||||
/**
|
||||
* If true, stream the file instead of saving it
|
||||
*/
|
||||
download?: boolean
|
||||
input: Export
|
||||
req: PayloadRequest
|
||||
user?: User
|
||||
}
|
||||
|
||||
export const createExport = async (args: CreateExportArgs) => {
|
||||
const {
|
||||
download,
|
||||
input: {
|
||||
id,
|
||||
name: nameArg,
|
||||
collectionSlug,
|
||||
exportsCollection,
|
||||
fields,
|
||||
format,
|
||||
locale: localeInput,
|
||||
sort,
|
||||
user,
|
||||
where,
|
||||
},
|
||||
req: { locale: localeArg, payload },
|
||||
req,
|
||||
} = args
|
||||
const locale = localeInput ?? localeArg
|
||||
const collectionConfig = payload.config.collections.find(({ slug }) => slug === collectionSlug)
|
||||
if (!collectionConfig) {
|
||||
throw new APIError(`Collection with slug ${collectionSlug} not found`)
|
||||
}
|
||||
|
||||
const name = `${nameArg ?? `${getFilename()}-${collectionSlug}`}.${format}`
|
||||
const isCSV = format === 'csv'
|
||||
|
||||
const findArgs = {
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
limit: 100,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
page: 0,
|
||||
select: fields ? getSelect(fields) : undefined,
|
||||
sort,
|
||||
user,
|
||||
where,
|
||||
}
|
||||
|
||||
let result: PaginatedDocs = { hasNextPage: true } as PaginatedDocs
|
||||
|
||||
if (download) {
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new Readable({
|
||||
async read() {
|
||||
let result = await payload.find(findArgs)
|
||||
let isFirstBatch = true
|
||||
|
||||
while (result.docs.length > 0) {
|
||||
const csvInput = result.docs.map((doc) => flattenObject(doc))
|
||||
const csvString = stringify(csvInput, { header: isFirstBatch })
|
||||
this.push(encoder.encode(csvString))
|
||||
isFirstBatch = false
|
||||
|
||||
if (!result.hasNextPage) {
|
||||
this.push(null) // End the stream
|
||||
break
|
||||
}
|
||||
|
||||
findArgs.page += 1
|
||||
result = await payload.find(findArgs)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream as any, {
|
||||
headers: {
|
||||
'Content-Disposition': `attachment; filename="${name}"`,
|
||||
'Content-Type': isCSV ? 'text/csv' : 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const outputData: string[] = []
|
||||
let isFirstBatch = true
|
||||
|
||||
while (result.hasNextPage) {
|
||||
findArgs.page += 1
|
||||
result = await payload.find(findArgs)
|
||||
|
||||
if (isCSV) {
|
||||
const csvInput = result.docs.map((doc) => flattenObject(doc))
|
||||
outputData.push(stringify(csvInput, { header: isFirstBatch }))
|
||||
isFirstBatch = false
|
||||
} else {
|
||||
const jsonInput = result.docs.map((doc) => JSON.stringify(doc))
|
||||
outputData.push(jsonInput.join(',\n'))
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(format === 'json' ? `[${outputData.join(',')}]` : outputData.join(''))
|
||||
|
||||
if (!id) {
|
||||
req.file = {
|
||||
name,
|
||||
data: buffer,
|
||||
mimetype: isCSV ? 'text/csv' : 'application/json',
|
||||
size: buffer.length,
|
||||
}
|
||||
} else {
|
||||
await req.payload.update({
|
||||
id,
|
||||
collection: exportsCollection,
|
||||
data: {},
|
||||
file: {
|
||||
name,
|
||||
data: buffer,
|
||||
mimetype: isCSV ? 'text/csv' : 'application/json',
|
||||
size: buffer.length,
|
||||
},
|
||||
user,
|
||||
})
|
||||
}
|
||||
}
|
||||
26
packages/plugin-import-export/src/export/download.ts
Normal file
26
packages/plugin-import-export/src/export/download.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { PayloadHandler } from 'payload'
|
||||
|
||||
import { APIError } from 'payload'
|
||||
|
||||
import { createExport } from './createExport.js'
|
||||
|
||||
export const download: PayloadHandler = async (req) => {
|
||||
let body
|
||||
if (typeof req?.json === 'function') {
|
||||
body = await req.json()
|
||||
}
|
||||
|
||||
if (!body || !body.data) {
|
||||
throw new APIError('Request data is required.')
|
||||
}
|
||||
|
||||
req.payload.logger.info(`Download request received ${body.data.collectionSlug}`)
|
||||
|
||||
body.data.user = req.user
|
||||
|
||||
return createExport({
|
||||
download: true,
|
||||
input: body.data,
|
||||
req,
|
||||
}) as Promise<Response>
|
||||
}
|
||||
23
packages/plugin-import-export/src/export/flattenObject.ts
Normal file
23
packages/plugin-import-export/src/export/flattenObject.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const flattenObject = (obj: any, prefix: string = ''): Record<string, unknown> => {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
const newKey = prefix ? `${prefix}_${key}` : key
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
Object.assign(result, flattenObject(item, `${newKey}_${index}`))
|
||||
} else {
|
||||
result[`${newKey}_${index}`] = item
|
||||
}
|
||||
})
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
Object.assign(result, flattenObject(value, newKey))
|
||||
} else {
|
||||
result[newKey] = value
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Config, TaskHandler, User } from 'payload'
|
||||
|
||||
import type { CreateExportArgs } from './createExport.js'
|
||||
|
||||
import { createExport } from './createExport.js'
|
||||
import { getFields } from './getFields.js'
|
||||
|
||||
export const getCreateCollectionExportTask = (config: Config): TaskHandler<any, string> => {
|
||||
const inputSchema = getFields(config).concat(
|
||||
{
|
||||
name: 'user',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'userCollection',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'exportsCollection',
|
||||
type: 'text',
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
// @ts-expect-error plugin tasks cannot have predefined type slug
|
||||
slug: 'createCollectionExport',
|
||||
handler: async ({ input, req }: CreateExportArgs) => {
|
||||
let user: undefined | User
|
||||
|
||||
if (input.userCollection && input.user) {
|
||||
user = (await req.payload.findByID({
|
||||
id: input.user,
|
||||
collection: input.userCollection,
|
||||
})) as User
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found')
|
||||
}
|
||||
|
||||
await createExport({ input, req, user })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
},
|
||||
inputSchema,
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'success',
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
189
packages/plugin-import-export/src/export/getFields.ts
Normal file
189
packages/plugin-import-export/src/export/getFields.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import type { Config, Field, SelectField } from 'payload'
|
||||
|
||||
import { getFilename } from './getFilename.js'
|
||||
|
||||
export const getFields = (config: Config): Field[] => {
|
||||
let localeField: SelectField | undefined
|
||||
if (config.localization) {
|
||||
localeField = {
|
||||
name: 'locale',
|
||||
type: 'select',
|
||||
admin: {
|
||||
width: '33%',
|
||||
},
|
||||
defaultValue: 'all',
|
||||
label: 'Locale',
|
||||
options: [
|
||||
{
|
||||
label: 'All Locales',
|
||||
value: 'all',
|
||||
},
|
||||
...config.localization.locales.map((locale) => ({
|
||||
label: typeof locale === 'string' ? locale : locale.label,
|
||||
value: typeof locale === 'string' ? locale : locale.code,
|
||||
})),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
defaultValue: () => getFilename(),
|
||||
label: 'File Name',
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'format',
|
||||
type: 'select',
|
||||
admin: {
|
||||
width: '33%',
|
||||
},
|
||||
defaultValue: 'csv',
|
||||
label: 'Export Format',
|
||||
options: [
|
||||
{
|
||||
label: 'CSV',
|
||||
value: 'csv',
|
||||
},
|
||||
{
|
||||
label: 'JSON',
|
||||
value: 'json',
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
admin: {
|
||||
placeholder: 'No limit',
|
||||
width: '33%',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sort',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '@payloadcms/plugin-import-export/rsc#SortBy',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
...(localeField ? [localeField] : []),
|
||||
{
|
||||
name: 'drafts',
|
||||
type: 'select',
|
||||
admin: {
|
||||
condition: (data) => {
|
||||
const collectionConfig = (config.collections ?? []).find(
|
||||
(collection) => collection.slug === data.collectionSlug,
|
||||
)
|
||||
return Boolean(
|
||||
typeof collectionConfig?.versions === 'object' &&
|
||||
collectionConfig?.versions?.drafts,
|
||||
)
|
||||
},
|
||||
width: '33%',
|
||||
},
|
||||
defaultValue: 'true',
|
||||
label: 'Drafts',
|
||||
options: [
|
||||
{
|
||||
label: 'True',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
label: 'False',
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: 'depth',
|
||||
// type: 'number',
|
||||
// admin: {
|
||||
// width: '33%',
|
||||
// },
|
||||
// defaultValue: 1,
|
||||
// required: true,
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
// virtual field for the UI component to modify the hidden `where` field
|
||||
name: 'selectionToUse',
|
||||
type: 'radio',
|
||||
defaultValue: 'all',
|
||||
options: [
|
||||
{
|
||||
label: 'Use current selection',
|
||||
value: 'currentSelection',
|
||||
},
|
||||
{
|
||||
label: 'Use current filters',
|
||||
value: 'currentFilters',
|
||||
},
|
||||
{
|
||||
label: 'Use all documents',
|
||||
value: 'all',
|
||||
},
|
||||
],
|
||||
virtual: true,
|
||||
},
|
||||
{
|
||||
name: 'fields',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '@payloadcms/plugin-import-export/rsc#FieldsToExport',
|
||||
},
|
||||
},
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'collectionSlug',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '@payloadcms/plugin-import-export/rsc#CollectionField',
|
||||
},
|
||||
hidden: true,
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'where',
|
||||
type: 'json',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '@payloadcms/plugin-import-export/rsc#WhereField',
|
||||
},
|
||||
},
|
||||
defaultValue: {},
|
||||
},
|
||||
],
|
||||
label: 'Export Options',
|
||||
},
|
||||
{
|
||||
name: 'preview',
|
||||
type: 'ui',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '@payloadcms/plugin-import-export/rsc#Preview',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
7
packages/plugin-import-export/src/export/getFilename.ts
Normal file
7
packages/plugin-import-export/src/export/getFilename.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const getFilename = () => {
|
||||
const now = new Date()
|
||||
const yyymmdd = now.toISOString().split('T')[0] // "YYYY-MM-DD"
|
||||
const hhmmss = now.toTimeString().split(' ')[0] // "HH:MM:SS"
|
||||
|
||||
return `${yyymmdd} ${hhmmss}`
|
||||
}
|
||||
31
packages/plugin-import-export/src/export/getSelect.ts
Normal file
31
packages/plugin-import-export/src/export/getSelect.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { SelectType } from 'payload'
|
||||
|
||||
/**
|
||||
* Takes an input of array of string paths in dot notation and returns a select object
|
||||
* example args: ['id', 'title', 'group.value', 'createdAt', 'updatedAt']
|
||||
*/
|
||||
export const getSelect = (fields: string[]): SelectType => {
|
||||
const select: SelectType = {}
|
||||
|
||||
fields.forEach((field) => {
|
||||
// TODO: this can likely be removed, the form was not saving, leaving in for now
|
||||
if (!field) {
|
||||
return
|
||||
}
|
||||
const segments = field.split('.')
|
||||
let selectRef = select
|
||||
|
||||
segments.forEach((segment, i) => {
|
||||
if (i === segments.length - 1) {
|
||||
selectRef[segment] = true
|
||||
} else {
|
||||
if (!selectRef[segment]) {
|
||||
selectRef[segment] = {}
|
||||
}
|
||||
selectRef = selectRef[segment] as SelectType
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return select
|
||||
}
|
||||
8
packages/plugin-import-export/src/exports/rsc.ts
Normal file
8
packages/plugin-import-export/src/exports/rsc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { CollectionField } from '../components/CollectionField/index.js'
|
||||
export { ExportListMenuItem } from '../components/ExportListMenuItem/index.js'
|
||||
export { ExportSaveButton } from '../components/ExportSaveButton/index.js'
|
||||
export { FieldsToExport } from '../components/FieldsToExport/index.js'
|
||||
export { ImportExportProvider } from '../components/ImportExportProvider/index.js'
|
||||
export { Preview } from '../components/Preview/index.js'
|
||||
export { SortBy } from '../components/SortBy/index.js'
|
||||
export { WhereField } from '../components/WhereField/index.js'
|
||||
1
packages/plugin-import-export/src/exports/types.ts
Normal file
1
packages/plugin-import-export/src/exports/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type { ImportExportPluginConfig } from '../types.js'
|
||||
88
packages/plugin-import-export/src/getExportCollection.ts
Normal file
88
packages/plugin-import-export/src/getExportCollection.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type {
|
||||
CollectionAfterChangeHook,
|
||||
CollectionBeforeChangeHook,
|
||||
CollectionBeforeOperationHook,
|
||||
CollectionConfig,
|
||||
Config,
|
||||
} from 'payload'
|
||||
|
||||
import type { CollectionOverride, ImportExportPluginConfig } from './types.js'
|
||||
|
||||
import { createExport } from './export/createExport.js'
|
||||
import { download } from './export/download.js'
|
||||
import { getFields } from './export/getFields.js'
|
||||
|
||||
export const getExportCollection = ({
|
||||
config,
|
||||
pluginConfig,
|
||||
}: {
|
||||
config: Config
|
||||
pluginConfig: ImportExportPluginConfig
|
||||
}): CollectionConfig => {
|
||||
const { overrideExportCollection } = pluginConfig
|
||||
|
||||
const beforeOperation: CollectionBeforeOperationHook[] = []
|
||||
const afterChange: CollectionAfterChangeHook[] = []
|
||||
|
||||
let collection: CollectionOverride = {
|
||||
slug: 'exports',
|
||||
access: {
|
||||
update: () => false,
|
||||
},
|
||||
admin: {
|
||||
group: false,
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
disableDuplicate: true,
|
||||
endpoints: [
|
||||
{
|
||||
handler: download,
|
||||
method: 'post',
|
||||
path: '/download',
|
||||
},
|
||||
],
|
||||
fields: getFields(config),
|
||||
hooks: {
|
||||
afterChange,
|
||||
beforeOperation,
|
||||
},
|
||||
upload: {
|
||||
filesRequiredOnCreate: false,
|
||||
hideFileInputOnCreate: true,
|
||||
hideRemoveFile: true,
|
||||
},
|
||||
}
|
||||
|
||||
if (typeof overrideExportCollection === 'function') {
|
||||
collection = overrideExportCollection(collection)
|
||||
}
|
||||
|
||||
if (pluginConfig.disableJobsQueue) {
|
||||
beforeOperation.push(async ({ args, operation, req }) => {
|
||||
if (operation !== 'create') {
|
||||
return
|
||||
}
|
||||
const { user } = req
|
||||
await createExport({ input: { ...args.data, user }, req })
|
||||
})
|
||||
} else {
|
||||
afterChange.push(async ({ doc, operation, req }) => {
|
||||
if (operation !== 'create') {
|
||||
return
|
||||
}
|
||||
|
||||
const input = {
|
||||
...doc,
|
||||
exportsCollection: collection.slug,
|
||||
user: req?.user?.id || req?.user?.user?.id,
|
||||
userCollection: 'users',
|
||||
}
|
||||
await req.payload.jobs.queue({
|
||||
input,
|
||||
task: 'createCollectionExport',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
72
packages/plugin-import-export/src/index.ts
Normal file
72
packages/plugin-import-export/src/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { Config, JobsConfig } from 'payload'
|
||||
|
||||
import { deepMergeSimple } from 'payload'
|
||||
|
||||
import type { ImportExportPluginConfig } from './types.js'
|
||||
|
||||
import { getCreateCollectionExportTask } from './export/getCreateExportCollectionTask.js'
|
||||
import { getExportCollection } from './getExportCollection.js'
|
||||
import { translations } from './translations/index.js'
|
||||
|
||||
export const importExportPlugin =
|
||||
(pluginConfig: ImportExportPluginConfig) =>
|
||||
(config: Config): Config => {
|
||||
const exportCollection = getExportCollection({ config, pluginConfig })
|
||||
if (config.collections) {
|
||||
config.collections.push(exportCollection)
|
||||
} else {
|
||||
config.collections = [exportCollection]
|
||||
}
|
||||
|
||||
// inject custom import export provider
|
||||
config.admin = config.admin || {}
|
||||
config.admin.components = config.admin.components || {}
|
||||
config.admin.components.providers = config.admin.components.providers || []
|
||||
config.admin.components.providers.push(
|
||||
'@payloadcms/plugin-import-export/rsc#ImportExportProvider',
|
||||
)
|
||||
|
||||
// inject the createExport job into the config
|
||||
config.jobs =
|
||||
config.jobs ||
|
||||
({
|
||||
tasks: [getCreateCollectionExportTask(config)],
|
||||
} as unknown as JobsConfig) // cannot type jobs config inside of plugins
|
||||
|
||||
let collectionsToUpdate = config.collections
|
||||
|
||||
const usePluginCollections = pluginConfig.collections && pluginConfig.collections?.length > 0
|
||||
|
||||
if (usePluginCollections) {
|
||||
collectionsToUpdate = config.collections?.filter((collection) => {
|
||||
return pluginConfig.collections?.includes(collection.slug)
|
||||
})
|
||||
}
|
||||
|
||||
collectionsToUpdate.forEach((collection) => {
|
||||
if (!collection.admin) {
|
||||
collection.admin = { components: { listMenuItems: [] } }
|
||||
}
|
||||
const components = collection.admin.components || {}
|
||||
if (!components.listMenuItems) {
|
||||
components.listMenuItems = []
|
||||
}
|
||||
if (!components.edit) {
|
||||
components.edit = {}
|
||||
}
|
||||
if (!components.edit.SaveButton) {
|
||||
components.edit.SaveButton = '@payloadcms/plugin-import-export/rsc#ExportSaveButton'
|
||||
}
|
||||
components.listMenuItems.push({
|
||||
clientProps: {
|
||||
exportCollectionSlug: exportCollection.slug,
|
||||
},
|
||||
path: '@payloadcms/plugin-import-export/rsc#ExportListMenuItem',
|
||||
})
|
||||
collection.admin.components = components
|
||||
})
|
||||
|
||||
config.i18n = deepMergeSimple(translations, config.i18n?.translations ?? {})
|
||||
|
||||
return config
|
||||
}
|
||||
9
packages/plugin-import-export/src/translations/en.ts
Normal file
9
packages/plugin-import-export/src/translations/en.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { GenericTranslationsObject } from '@payloadcms/translations'
|
||||
|
||||
export const en: GenericTranslationsObject = {
|
||||
$schema: './translation-schema.json',
|
||||
'plugin-seo': {
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
},
|
||||
}
|
||||
11
packages/plugin-import-export/src/translations/index.ts
Normal file
11
packages/plugin-import-export/src/translations/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { GenericTranslationsObject, NestedKeysStripped } from '@payloadcms/translations'
|
||||
|
||||
import { en } from './en.js'
|
||||
|
||||
export const translations = {
|
||||
en,
|
||||
}
|
||||
|
||||
export type PluginImportExportTranslations = GenericTranslationsObject
|
||||
|
||||
export type PluginImportExportTranslationKeys = NestedKeysStripped<PluginImportExportTranslations>
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "object",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugin-import-export": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"export": {
|
||||
"type": "string"
|
||||
},
|
||||
"import": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["export", "import"]
|
||||
}
|
||||
},
|
||||
"required": ["plugin-import-export"]
|
||||
}
|
||||
24
packages/plugin-import-export/src/types.ts
Normal file
24
packages/plugin-import-export/src/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { CollectionAdminOptions, CollectionConfig, UploadConfig } from 'payload'
|
||||
|
||||
export type CollectionOverride = {
|
||||
admin: CollectionAdminOptions
|
||||
upload: UploadConfig
|
||||
} & CollectionConfig
|
||||
|
||||
export type ImportExportPluginConfig = {
|
||||
/**
|
||||
* Collections to include the Import/Export controls in
|
||||
* Defaults to all collections
|
||||
*/
|
||||
collections?: string[]
|
||||
/**
|
||||
* Enable to force the export to run synchronously
|
||||
*/
|
||||
disableJobsQueue?: boolean
|
||||
/**
|
||||
* This function takes the default export collection configured in the plugin and allows you to override it by modifying and returning it
|
||||
* @param collection
|
||||
* @returns collection
|
||||
*/
|
||||
overrideExportCollection?: (collection: CollectionOverride) => CollectionOverride
|
||||
}
|
||||
25
packages/plugin-import-export/tsconfig.json
Normal file
25
packages/plugin-import-export/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||
"noEmit": false /* Do not emit outputs. */,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"tests",
|
||||
"test",
|
||||
"node_modules",
|
||||
"eslint.config.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/**/*.ts", "src/**/**/*.tsx", "src/**/*.d.ts", "src/**/*.json", ],
|
||||
"references": [{ "path": "../payload" }, { "path": "../ui"}]
|
||||
}
|
||||
44
pnpm-lock.yaml
generated
44
pnpm-lock.yaml
generated
@@ -1025,6 +1025,34 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
|
||||
packages/plugin-import-export:
|
||||
dependencies:
|
||||
'@faceless-ui/modal':
|
||||
specifier: 3.0.0-beta.2
|
||||
version: 3.0.0-beta.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@payloadcms/translations':
|
||||
specifier: workspace:*
|
||||
version: link:../translations
|
||||
'@payloadcms/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../ui
|
||||
csv-parse:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
csv-stringify:
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.2
|
||||
qs-esm:
|
||||
specifier: 7.0.2
|
||||
version: 7.0.2
|
||||
devDependencies:
|
||||
'@payloadcms/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config
|
||||
payload:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
|
||||
packages/plugin-multi-tenant:
|
||||
dependencies:
|
||||
next:
|
||||
@@ -1677,6 +1705,9 @@ importers:
|
||||
'@payloadcms/plugin-form-builder':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/plugin-form-builder
|
||||
'@payloadcms/plugin-import-export':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/plugin-import-export
|
||||
'@payloadcms/plugin-multi-tenant':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/plugin-multi-tenant
|
||||
@@ -1749,6 +1780,9 @@ importers:
|
||||
create-payload-app:
|
||||
specifier: workspace:*
|
||||
version: link:../packages/create-payload-app
|
||||
csv-parse:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
dequal:
|
||||
specifier: 2.0.3
|
||||
version: 2.0.3
|
||||
@@ -6302,6 +6336,12 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
csv-parse@5.6.0:
|
||||
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
|
||||
|
||||
csv-stringify@6.5.2:
|
||||
resolution: {integrity: sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
@@ -15963,6 +16003,10 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
csv-parse@5.6.0: {}
|
||||
|
||||
csv-stringify@6.5.2: {}
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
@@ -1235,3 +1235,4 @@ declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@payloadcms/payload-cloud": "workspace:*",
|
||||
"@payloadcms/plugin-cloud-storage": "workspace:*",
|
||||
"@payloadcms/plugin-form-builder": "workspace:*",
|
||||
"@payloadcms/plugin-import-export": "workspace:*",
|
||||
"@payloadcms/plugin-multi-tenant": "workspace:*",
|
||||
"@payloadcms/plugin-nested-docs": "workspace:*",
|
||||
"@payloadcms/plugin-redirects": "workspace:*",
|
||||
@@ -65,6 +66,7 @@
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"comment-json": "^4.2.3",
|
||||
"create-payload-app": "workspace:*",
|
||||
"csv-parse": "^5.6.0",
|
||||
"dequal": "2.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"drizzle-kit": "0.28.0",
|
||||
|
||||
1
test/plugin-import-export/.gitignore
vendored
Normal file
1
test/plugin-import-export/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
uploads
|
||||
117
test/plugin-import-export/collections/Pages.ts
Normal file
117
test/plugin-import-export/collections/Pages.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { pagesSlug } from '../shared.js'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: pagesSlug,
|
||||
labels: {
|
||||
singular: 'Page',
|
||||
plural: 'Pages',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'localized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'value',
|
||||
type: 'text',
|
||||
defaultValue: 'group value',
|
||||
},
|
||||
{
|
||||
name: 'ignore',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'field2',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'field1',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'field2',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'hero',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'content',
|
||||
fields: [
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
},
|
||||
{
|
||||
name: 'hasManyNumber',
|
||||
type: 'number',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'relationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
label: 'Excerpt',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
16
test/plugin-import-export/collections/Users.ts
Normal file
16
test/plugin-import-export/collections/Users.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
],
|
||||
}
|
||||
51
test/plugin-import-export/config.ts
Normal file
51
test/plugin-import-export/config.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
import { importExportPlugin } from '@payloadcms/plugin-import-export'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { Pages } from './collections/Pages.js'
|
||||
import { Users } from './collections/Users.js'
|
||||
import { seed } from './seed/index.js'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [Users, Pages],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
locales: ['en', 'es', 'de'],
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await seed(payload)
|
||||
},
|
||||
plugins: [
|
||||
importExportPlugin({
|
||||
overrideExportCollection: (collection) => {
|
||||
collection.admin.group = 'System'
|
||||
collection.upload.staticDir = path.resolve(dirname, 'uploads')
|
||||
return collection
|
||||
},
|
||||
disableJobsQueue: true,
|
||||
}),
|
||||
importExportPlugin({
|
||||
collections: ['pages'],
|
||||
overrideExportCollection: (collection) => {
|
||||
collection.slug = 'exports-tasks'
|
||||
if (collection.admin) {
|
||||
collection.admin.group = 'System'
|
||||
}
|
||||
collection.upload.staticDir = path.resolve(dirname, 'uploads')
|
||||
return collection
|
||||
},
|
||||
}),
|
||||
],
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
50
test/plugin-import-export/e2e.spec.ts
Normal file
50
test/plugin-import-export/e2e.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||
import type { Config } from './payload-types.js'
|
||||
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
test.describe('Import Export', () => {
|
||||
let page: Page
|
||||
let pagesURL: AdminUrlUtil
|
||||
let payload: PayloadTestSDK<Config>
|
||||
|
||||
test.beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
const { payload: payloadFromInit, serverURL } = await initPayloadE2ENoConfig<Config>({
|
||||
dirname,
|
||||
})
|
||||
pagesURL = new AdminUrlUtil(serverURL, 'pages')
|
||||
|
||||
payload = payloadFromInit
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
test.describe('Import', () => {
|
||||
test('works', async () => {
|
||||
// TODO: write e2e tests
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Export', () => {
|
||||
test('works', async () => {
|
||||
// TODO: write e2e tests
|
||||
})
|
||||
})
|
||||
})
|
||||
19
test/plugin-import-export/eslint.config.js
Normal file
19
test/plugin-import-export/eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import testEslintConfig from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
...rootParserOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
51
test/plugin-import-export/helpers.ts
Normal file
51
test/plugin-import-export/helpers.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { parse } from 'csv-parse'
|
||||
import fs from 'fs'
|
||||
|
||||
export const readCSV = async (path: string): Promise<any[]> => {
|
||||
const buffer = fs.readFileSync(path)
|
||||
const data: any[] = []
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
const parser = parse({ bom: true, columns: true })
|
||||
|
||||
// Collect data from the CSV
|
||||
parser.on('readable', () => {
|
||||
let record
|
||||
while ((record = parser.read())) {
|
||||
data.push(record)
|
||||
}
|
||||
})
|
||||
|
||||
// Resolve the promise on 'end'
|
||||
parser.on('end', () => {
|
||||
resolve()
|
||||
})
|
||||
|
||||
// Handle errors (optional, but good practice)
|
||||
parser.on('error', (error) => {
|
||||
console.error('Error parsing CSV:', error)
|
||||
resolve() // Ensures promise doesn't hang on error
|
||||
})
|
||||
|
||||
// Pipe the buffer into the parser
|
||||
parser.write(buffer)
|
||||
parser.end()
|
||||
})
|
||||
|
||||
// Await the promise
|
||||
await promise
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export const readJSON = async (path: string): Promise<any[]> => {
|
||||
const buffer = fs.readFileSync(path)
|
||||
const str = buffer.toString()
|
||||
|
||||
try {
|
||||
const json = await JSON.parse(str)
|
||||
return json
|
||||
} catch (error) {
|
||||
console.error('Error reading JSON file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
400
test/plugin-import-export/int.spec.ts
Normal file
400
test/plugin-import-export/int.spec.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import type { CollectionSlug, Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { readCSV, readJSON } from './helpers.js'
|
||||
import { richTextData } from './seed/richTextData.js'
|
||||
|
||||
let payload: Payload
|
||||
let user: any
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('@payloadcms/plugin-import-export', () => {
|
||||
beforeAll(async () => {
|
||||
;({ payload } = (await initPayloadInt(dirname)) as { payload: Payload })
|
||||
user = await payload.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload.db.destroy === 'function') {
|
||||
await payload.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
describe('exports', () => {
|
||||
it('should create a file for collection csv from defined fields', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
sort: 'createdAt',
|
||||
fields: ['id', 'title', 'group.value', 'group.array.field1', 'createdAt', 'updatedAt'],
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Title ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toContain('pages.csv')
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].id).toBeDefined()
|
||||
expect(data[0].title).toStrictEqual('Title 0')
|
||||
expect(data[0].group_value).toStrictEqual('group value')
|
||||
expect(data[0].group_ignore).toBeUndefined()
|
||||
expect(data[0].group_array_0_field1).toStrictEqual('test')
|
||||
expect(data[0].createdAt).toBeDefined()
|
||||
expect(data[0].updatedAt).toBeDefined()
|
||||
})
|
||||
|
||||
it('should create a file for collection csv from one locale', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'localized'],
|
||||
locale: 'en',
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Localized ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].id).toBeDefined()
|
||||
expect(data[0].localized).toStrictEqual('en test')
|
||||
})
|
||||
|
||||
it('should create a file for collection csv from multiple locales', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'localized'],
|
||||
locale: 'all',
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Localized ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].id).toBeDefined()
|
||||
expect(data[0].localized_en).toStrictEqual('en test')
|
||||
expect(data[0].localized_es).toStrictEqual('es test')
|
||||
})
|
||||
|
||||
it('should create a file for collection csv from array', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'array'],
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Array ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].array_0_field1).toStrictEqual('foo')
|
||||
expect(data[0].array_0_field2).toStrictEqual('bar')
|
||||
expect(data[0].array_1_field1).toStrictEqual('foo')
|
||||
expect(data[0].array_1_field2).toStrictEqual('baz')
|
||||
})
|
||||
|
||||
it('should create a file for collection csv from array.subfield', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'array.field1'],
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Array Subfield ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].array_0_field1).toStrictEqual('foo')
|
||||
expect(data[0].array_0_field2).toBeUndefined()
|
||||
expect(data[0].array_1_field1).toStrictEqual('foo')
|
||||
expect(data[0].array_1_field2).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should create a file for collection csv from hasMany field', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'hasManyNumber'],
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'hasMany Number ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].hasManyNumber_0).toStrictEqual('0')
|
||||
expect(data[0].hasManyNumber_1).toStrictEqual('1')
|
||||
expect(data[0].hasManyNumber_2).toStrictEqual('1')
|
||||
expect(data[0].hasManyNumber_3).toStrictEqual('2')
|
||||
expect(data[0].hasManyNumber_4).toStrictEqual('3')
|
||||
})
|
||||
|
||||
it('should create a file for collection csv from blocks field', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'blocks'],
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Blocks ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].blocks_0_blockType).toStrictEqual('hero')
|
||||
expect(data[0].blocks_1_blockType).toStrictEqual('content')
|
||||
})
|
||||
|
||||
it('should create a JSON file for collection', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'title'],
|
||||
format: 'json',
|
||||
sort: 'title',
|
||||
where: {
|
||||
title: { contains: 'JSON ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readJSON(expectedPath)
|
||||
|
||||
expect(data[0].title).toStrictEqual('JSON 0')
|
||||
})
|
||||
|
||||
it('should create an export with every field when no fields are defined', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
format: 'json',
|
||||
sort: 'title',
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readJSON(expectedPath)
|
||||
|
||||
expect(data[0].id).toBeDefined()
|
||||
expect(data[0].title).toBeDefined()
|
||||
expect(data[0].createdAt).toBeDefined()
|
||||
expect(data[0].updatedAt).toBeDefined()
|
||||
})
|
||||
|
||||
it('should create jobs task for exports', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'exports-tasks' as CollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'title'],
|
||||
format: 'csv',
|
||||
sort: 'title',
|
||||
where: {
|
||||
title: { contains: 'Jobs ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection: 'payload-jobs' as CollectionSlug,
|
||||
})
|
||||
const job = docs[0]
|
||||
|
||||
expect(job).toBeDefined()
|
||||
|
||||
const { input } = job
|
||||
|
||||
expect(input.id).toBeDefined()
|
||||
expect(input.name).toBeDefined()
|
||||
expect(input.format).toStrictEqual('csv')
|
||||
expect(input.locale).toStrictEqual('all')
|
||||
expect(input.fields).toStrictEqual(['id', 'title'])
|
||||
expect(input.collectionSlug).toStrictEqual('pages')
|
||||
expect(input.exportsCollection).toStrictEqual('exports-tasks')
|
||||
expect(input.user).toBeDefined()
|
||||
expect(input.userCollection).toBeDefined()
|
||||
|
||||
await payload.jobs.run()
|
||||
|
||||
const exportDoc = await payload.findByID({
|
||||
collection: 'exports-tasks' as CollectionSlug,
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(exportDoc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', exportDoc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].title).toStrictEqual('Jobs 0')
|
||||
})
|
||||
|
||||
// disabled so we don't always run a massive test
|
||||
it.skip('should create a file from a large set of collection documents', async () => {
|
||||
const allPromises = []
|
||||
let promises = []
|
||||
for (let i = 0; i < 100000; i++) {
|
||||
promises.push(
|
||||
payload.create({
|
||||
collectionSlug: 'pages',
|
||||
data: {
|
||||
title: `Array ${i}`,
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'hero',
|
||||
title: 'test',
|
||||
},
|
||||
{
|
||||
blockType: 'content',
|
||||
richText: richTextData,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
if (promises.length >= 500) {
|
||||
await Promise.all(promises)
|
||||
promises = []
|
||||
}
|
||||
if (i % 1000 === 0) {
|
||||
console.log('created', i)
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
console.log('seeded')
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields: ['id', 'blocks'],
|
||||
format: 'csv',
|
||||
},
|
||||
})
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(doc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
expect(data[0].blocks_0_blockType).toStrictEqual('hero')
|
||||
expect(data[0].blocks_1_blockType).toStrictEqual('content')
|
||||
})
|
||||
})
|
||||
})
|
||||
670
test/plugin-import-export/payload-types.ts
Normal file
670
test/plugin-import-export/payload-types.ts
Normal file
@@ -0,0 +1,670 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
pages: Page;
|
||||
exports: Export;
|
||||
'exports-tasks': ExportsTask;
|
||||
'payload-jobs': PayloadJob;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
exports: ExportsSelect<false> | ExportsSelect<true>;
|
||||
'exports-tasks': ExportsTasksSelect<false> | ExportsTasksSelect<true>;
|
||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: 'en' | 'es' | 'de';
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: {
|
||||
createCollectionExport: TaskCreateCollectionExport;
|
||||
inline: {
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
};
|
||||
};
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string;
|
||||
title: string;
|
||||
localized?: string | null;
|
||||
group?: {
|
||||
value?: string | null;
|
||||
ignore?: string | null;
|
||||
array?:
|
||||
| {
|
||||
field1?: string | null;
|
||||
field2?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
array?:
|
||||
| {
|
||||
field1?: string | null;
|
||||
field2?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
blocks?:
|
||||
| (
|
||||
| {
|
||||
title?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'hero';
|
||||
}
|
||||
| {
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'content';
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
author?: (string | null) | User;
|
||||
hasManyNumber?: number[] | null;
|
||||
relationship?: (string | null) | User;
|
||||
excerpt?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "exports".
|
||||
*/
|
||||
export interface Export {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
format: 'csv' | 'json';
|
||||
limit?: number | null;
|
||||
sort?: string | null;
|
||||
locale?: ('all' | 'en' | 'es' | 'de') | null;
|
||||
drafts?: ('true' | 'false') | null;
|
||||
selectionToUse?: ('currentSelection' | 'currentFilters' | 'all') | null;
|
||||
fields?: string[] | null;
|
||||
collectionSlug: string;
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "exports-tasks".
|
||||
*/
|
||||
export interface ExportsTask {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
format: 'csv' | 'json';
|
||||
limit?: number | null;
|
||||
sort?: string | null;
|
||||
locale?: ('all' | 'en' | 'es' | 'de') | null;
|
||||
drafts?: ('true' | 'false') | null;
|
||||
selectionToUse?: ('currentSelection' | 'currentFilters' | 'all') | null;
|
||||
fields?: string[] | null;
|
||||
collectionSlug: string;
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-jobs".
|
||||
*/
|
||||
export interface PayloadJob {
|
||||
id: string;
|
||||
/**
|
||||
* Input data provided to the job
|
||||
*/
|
||||
input?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
taskStatus?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
completedAt?: string | null;
|
||||
totalTried?: number | null;
|
||||
/**
|
||||
* If hasError is true this job will not be retried
|
||||
*/
|
||||
hasError?: boolean | null;
|
||||
/**
|
||||
* If hasError is true, this is the error that caused it
|
||||
*/
|
||||
error?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* Task execution log
|
||||
*/
|
||||
log?:
|
||||
| {
|
||||
executedAt: string;
|
||||
completedAt: string;
|
||||
taskSlug: 'inline' | 'createCollectionExport';
|
||||
taskID: string;
|
||||
input?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
output?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
state: 'failed' | 'succeeded';
|
||||
error?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
taskSlug?: ('inline' | 'createCollectionExport') | null;
|
||||
queue?: string | null;
|
||||
waitUntil?: string | null;
|
||||
processing?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'exports';
|
||||
value: string | Export;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'exports-tasks';
|
||||
value: string | ExportsTask;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'payload-jobs';
|
||||
value: string | PayloadJob;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
localized?: T;
|
||||
group?:
|
||||
| T
|
||||
| {
|
||||
value?: T;
|
||||
ignore?: T;
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
field1?: T;
|
||||
field2?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
field1?: T;
|
||||
field2?: T;
|
||||
id?: T;
|
||||
};
|
||||
blocks?:
|
||||
| T
|
||||
| {
|
||||
hero?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
content?:
|
||||
| T
|
||||
| {
|
||||
richText?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
author?: T;
|
||||
hasManyNumber?: T;
|
||||
relationship?: T;
|
||||
excerpt?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "exports_select".
|
||||
*/
|
||||
export interface ExportsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
format?: T;
|
||||
limit?: T;
|
||||
sort?: T;
|
||||
locale?: T;
|
||||
drafts?: T;
|
||||
selectionToUse?: T;
|
||||
fields?: T;
|
||||
collectionSlug?: T;
|
||||
where?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "exports-tasks_select".
|
||||
*/
|
||||
export interface ExportsTasksSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
format?: T;
|
||||
limit?: T;
|
||||
sort?: T;
|
||||
locale?: T;
|
||||
drafts?: T;
|
||||
selectionToUse?: T;
|
||||
fields?: T;
|
||||
collectionSlug?: T;
|
||||
where?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-jobs_select".
|
||||
*/
|
||||
export interface PayloadJobsSelect<T extends boolean = true> {
|
||||
input?: T;
|
||||
taskStatus?: T;
|
||||
completedAt?: T;
|
||||
totalTried?: T;
|
||||
hasError?: T;
|
||||
error?: T;
|
||||
log?:
|
||||
| T
|
||||
| {
|
||||
executedAt?: T;
|
||||
completedAt?: T;
|
||||
taskSlug?: T;
|
||||
taskID?: T;
|
||||
input?: T;
|
||||
output?: T;
|
||||
state?: T;
|
||||
error?: T;
|
||||
id?: T;
|
||||
};
|
||||
taskSlug?: T;
|
||||
queue?: T;
|
||||
waitUntil?: T;
|
||||
processing?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskCreateCollectionExport".
|
||||
*/
|
||||
export interface TaskCreateCollectionExport {
|
||||
input: {
|
||||
name?: string | null;
|
||||
format: 'csv' | 'json';
|
||||
limit?: number | null;
|
||||
sort?: string | null;
|
||||
locale?: ('all' | 'en' | 'es' | 'de') | null;
|
||||
drafts?: ('true' | 'false') | null;
|
||||
selectionToUse?: ('currentSelection' | 'currentFilters' | 'all') | null;
|
||||
fields?: string[] | null;
|
||||
collectionSlug: string;
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
user?: string | null;
|
||||
userCollection?: string | null;
|
||||
exportsCollection?: string | null;
|
||||
};
|
||||
output: {
|
||||
success?: boolean | null;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
135
test/plugin-import-export/seed/index.ts
Normal file
135
test/plugin-import-export/seed/index.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { devUser } from '../../credentials.js'
|
||||
import { richTextData } from './richTextData.js'
|
||||
|
||||
export const seed = async (payload: Payload): Promise<boolean> => {
|
||||
payload.logger.info('Seeding data...')
|
||||
try {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
// create pages
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Title ${i}`,
|
||||
group: {
|
||||
array: [{ field1: 'test' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const doc = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Localized ${i}`,
|
||||
localized: 'en test',
|
||||
},
|
||||
locale: 'en',
|
||||
})
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: doc.id,
|
||||
data: {
|
||||
localized: 'es test',
|
||||
},
|
||||
locale: 'es',
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Array ${i}`,
|
||||
array: [
|
||||
{
|
||||
field1: 'foo',
|
||||
field2: 'bar',
|
||||
},
|
||||
{
|
||||
field1: 'foo',
|
||||
field2: 'baz',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Array Subfield ${i}`,
|
||||
array: [
|
||||
{
|
||||
field1: 'foo',
|
||||
field2: 'bar',
|
||||
},
|
||||
{
|
||||
field1: 'foo',
|
||||
field2: 'baz',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `hasMany Number ${i}`,
|
||||
hasManyNumber: [0, 1, 1, 2, 3, 5, 8, 13, 21],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Blocks ${i}`,
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'hero',
|
||||
title: 'test',
|
||||
},
|
||||
{
|
||||
blockType: 'content',
|
||||
richText: richTextData,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `JSON ${i}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Jobs ${i}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
110
test/plugin-import-export/seed/richTextData.ts
Normal file
110
test/plugin-import-export/seed/richTextData.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
export const richTextData = {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'This is some content in a block',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h2',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Paragraph of text',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Testing ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Richtext',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'list',
|
||||
version: 1,
|
||||
listType: 'number',
|
||||
start: 1,
|
||||
tag: 'ol',
|
||||
},
|
||||
{
|
||||
children: [],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'root',
|
||||
version: 1,
|
||||
},
|
||||
}
|
||||
1
test/plugin-import-export/shared.ts
Normal file
1
test/plugin-import-export/shared.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const pagesSlug = 'pages'
|
||||
13
test/plugin-import-export/tsconfig.eslint.json
Normal file
13
test/plugin-import-export/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
3
test/plugin-import-export/tsconfig.json
Normal file
3
test/plugin-import-export/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export const tgzToPkgNameMap = {
|
||||
'@payloadcms/payload-cloud': 'payloadcms-payload-cloud-*',
|
||||
'@payloadcms/plugin-cloud-storage': 'payloadcms-plugin-cloud-storage-*',
|
||||
'@payloadcms/plugin-form-builder': 'payloadcms-plugin-form-builder-*',
|
||||
'@payloadcms/plugin-import-export': 'payloadcms-plugin-import-export-*',
|
||||
'@payloadcms/plugin-multi-tenant': 'payloadcms-plugin-multi-tenant-*',
|
||||
'@payloadcms/plugin-nested-docs': 'payloadcms-plugin-nested-docs-*',
|
||||
'@payloadcms/plugin-redirects': 'payloadcms-plugin-redirects-*',
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/_community/config.ts"],
|
||||
"@payload-config": ["./test/plugin-import-export/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
@@ -55,6 +55,9 @@
|
||||
"@payloadcms/plugin-form-builder/client": [
|
||||
"./packages/plugin-form-builder/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-import-export/rsc": [
|
||||
"./packages/plugin-import-export/src/exports/rsc.ts"
|
||||
],
|
||||
"@payloadcms/plugin-multi-tenant/rsc": ["./packages/plugin-multi-tenant/src/exports/rsc.ts"],
|
||||
"@payloadcms/plugin-multi-tenant/utilities": [
|
||||
"./packages/plugin-multi-tenant/src/exports/utilities.ts"
|
||||
@@ -68,13 +71,11 @@
|
||||
"@payloadcms/plugin-multi-tenant": ["./packages/plugin-multi-tenant/src/index.ts"],
|
||||
"@payloadcms/next": ["./packages/next/src/exports/*"],
|
||||
"@payloadcms/storage-s3/client": ["./packages/storage-s3/src/exports/client.ts"],
|
||||
"@payloadcms/storage-vercel-blob/client": [
|
||||
"./packages/storage-vercel-blob/src/exports/client.ts"
|
||||
"@payloadcms/storage-vercel-blob/client": ["./packages/storage-vercel-blob/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/storage-gcs/client": ["./packages/storage-gcs/src/exports/client.ts"],
|
||||
"@payloadcms/storage-uploadthing/client": [
|
||||
"./packages/storage-uploadthing/src/exports/client.ts"
|
||||
]
|
||||
"./packages/storage-uploadthing/src/exports/client.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["${configDir}/src"],
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
{
|
||||
"path": "./packages/plugin-seo"
|
||||
},
|
||||
{
|
||||
"path": "./packages/plugin-import-export"
|
||||
},
|
||||
{
|
||||
"path": "./packages/plugin-stripe"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user