Compare commits
41 Commits
main
...
feat/expor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebfc4b5936 | ||
|
|
fb90315307 | ||
|
|
2418d56824 | ||
|
|
5ea3c276c1 | ||
|
|
3f12fdfa8c | ||
|
|
2866a2d006 | ||
|
|
be6afb5e8f | ||
|
|
83eb2cebdb | ||
|
|
3fb057a2dc | ||
|
|
93062f6e37 | ||
|
|
b924ca9f5f | ||
|
|
3a420a6e4e | ||
|
|
66f8029873 | ||
|
|
4368560b0d | ||
|
|
88ee2aab04 | ||
|
|
094157aaf0 | ||
|
|
868daeb03b | ||
|
|
66d69cbb8f | ||
|
|
b67bea0011 | ||
|
|
a8ef7869a4 | ||
|
|
4bbf6ab193 | ||
|
|
1b145ff28f | ||
|
|
7fe74f0c67 | ||
|
|
f0dc521718 | ||
|
|
1ce8eb5af8 | ||
|
|
033cd8c7d9 | ||
|
|
ebc8ea62af | ||
|
|
25be18a023 | ||
|
|
bf9b9c7b3b | ||
|
|
8436b0ba68 | ||
|
|
11b2c3ebb6 | ||
|
|
57800b26da | ||
|
|
fa63ead3ab | ||
|
|
c5b4333fbd | ||
|
|
bfbc5f4d7b | ||
|
|
95bac9b5dd | ||
|
|
9db0603229 | ||
|
|
3e76465f77 | ||
|
|
b75ac5692f | ||
|
|
7f36c138cd | ||
|
|
299f1459e0 |
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -311,6 +311,7 @@ jobs:
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-import-export
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- versions
|
||||
|
||||
@@ -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\"",
|
||||
|
||||
@@ -33,6 +33,15 @@ export const renderListViewSlots = ({
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.afterListControls) {
|
||||
result.AfterListControls = RenderServerComponent({
|
||||
clientProps,
|
||||
Component: collectionConfig.admin.components.afterListControls,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.afterListTable) {
|
||||
result.AfterListTable = RenderServerComponent({
|
||||
clientProps,
|
||||
|
||||
@@ -30,6 +30,7 @@ export function iterateCollections({
|
||||
})
|
||||
|
||||
addToImportMap(collection.admin?.components?.afterList)
|
||||
addToImportMap(collection.admin?.components?.afterListControls)
|
||||
addToImportMap(collection.admin?.components?.afterListTable)
|
||||
addToImportMap(collection.admin?.components?.beforeList)
|
||||
addToImportMap(collection.admin?.components?.beforeListTable)
|
||||
|
||||
@@ -276,6 +276,7 @@ export type CollectionAdminOptions = {
|
||||
*/
|
||||
components?: {
|
||||
afterList?: CustomComponent[]
|
||||
afterListControls?: CustomComponent[]
|
||||
afterListTable?: CustomComponent[]
|
||||
beforeList?: CustomComponent[]
|
||||
beforeListTable?: CustomComponent[]
|
||||
|
||||
@@ -9,7 +9,7 @@ const traverseArrayOrBlocksField = ({
|
||||
fillEmpty,
|
||||
parentRef,
|
||||
}: {
|
||||
callback: TraverseFieldsCallback
|
||||
callback: TraverseFieldsCallback<unknown>
|
||||
data: Record<string, unknown>[]
|
||||
field: ArrayField | BlocksField
|
||||
fillEmpty: boolean
|
||||
@@ -41,7 +41,7 @@ const traverseArrayOrBlocksField = ({
|
||||
}
|
||||
}
|
||||
|
||||
export type TraverseFieldsCallback = (args: {
|
||||
export type TraverseFieldsCallback<T = unknown> = (args: {
|
||||
/**
|
||||
* The current field
|
||||
*/
|
||||
@@ -53,19 +53,19 @@ export type TraverseFieldsCallback = (args: {
|
||||
/**
|
||||
* The parent reference object
|
||||
*/
|
||||
parentRef?: Record<string, unknown> | unknown
|
||||
parentRef: Record<string, T> | T
|
||||
/**
|
||||
* The current reference object
|
||||
*/
|
||||
ref?: Record<string, unknown> | unknown
|
||||
ref: Record<string, T> | T
|
||||
}) => boolean | void
|
||||
|
||||
type TraverseFieldsArgs = {
|
||||
callback: TraverseFieldsCallback
|
||||
type TraverseFieldsArgs<T = unknown> = {
|
||||
callback: TraverseFieldsCallback<T>
|
||||
fields: (Field | TabAsField)[]
|
||||
fillEmpty?: boolean
|
||||
parentRef?: Record<string, unknown> | unknown
|
||||
ref?: Record<string, unknown> | unknown
|
||||
parentRef?: Record<string, T> | T
|
||||
ref?: Record<string, T> | T
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,13 +77,13 @@ type TraverseFieldsArgs = {
|
||||
* @param ref the data or any artifacts assigned in the callback during field recursion
|
||||
* @param parentRef the data or any artifacts assigned in the callback during field recursion one level up
|
||||
*/
|
||||
export const traverseFields = ({
|
||||
export const traverseFields = <T = unknown>({
|
||||
callback,
|
||||
fields,
|
||||
fillEmpty = true,
|
||||
parentRef = {},
|
||||
ref = {},
|
||||
}: TraverseFieldsArgs): void => {
|
||||
}: TraverseFieldsArgs<T>): void => {
|
||||
fields.some((field) => {
|
||||
let skip = false
|
||||
const next = () => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
17
packages/plugin-import-export/README.md
Normal file
17
packages/plugin-import-export/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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)
|
||||
|
||||
[//]: # 'TODO: Remove requirements'
|
||||
|
||||
## Requirements
|
||||
|
||||
### Exports
|
||||
|
||||
- [ ] The export button should be visible on the collection list.
|
||||
|
||||
Create writable streams for each collection and write the data to the streams. The streams should be piped to a zip stream and sent to the client.
|
||||
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.
|
||||
100
packages/plugin-import-export/package.json
Normal file
100
packages/plugin-import-export/package.json
Normal file
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.6.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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
27
packages/plugin-import-export/src/components/Dots/index.scss
Normal file
27
packages/plugin-import-export/src/components/Dots/index.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
@import '../../scss/styles';
|
||||
|
||||
@layer payload-default {
|
||||
.dots {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
background-color: var(--theme-elevation-150);
|
||||
border-radius: $style-radius-m;
|
||||
height: calc(var(--base) * 1.2);
|
||||
width: calc(var(--base) * 1.2);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 2.5px;
|
||||
height: 2.5px;
|
||||
border-radius: 100%;
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/plugin-import-export/src/components/Dots/index.tsx
Normal file
11
packages/plugin-import-export/src/components/Dots/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const Dots: React.FC<{ className?: string }> = ({ className }) => (
|
||||
<div className={[className && className, 'dots'].filter(Boolean).join(' ')}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
.export-button {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
import { useModal } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { ExportDrawer } from '../ExportDrawer/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'export-button'
|
||||
export const ExportButton: React.FC<{ collectionSlug: string; exportCollectionSlug: string }> = ({
|
||||
collectionSlug,
|
||||
exportCollectionSlug,
|
||||
}) => {
|
||||
const { toggleModal } = useModal()
|
||||
const exportDrawerSlug = `export-${collectionSlug}`
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* <button className={baseClass} onClick={() => toggleModal(exportDrawerSlug)} type="button">
|
||||
Export
|
||||
</button> */}
|
||||
<ExportDrawer
|
||||
collectionSlug={collectionSlug}
|
||||
drawerSlug={exportDrawerSlug}
|
||||
exportCollectionSlug={exportCollectionSlug}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { CollectionConfig, FormState, SanitizedLocalizationConfig } from 'payload'
|
||||
|
||||
import { getFilename } from '../../export/getFilename.js'
|
||||
|
||||
export const useInitialState = ({
|
||||
collectionConfig,
|
||||
localization,
|
||||
}: {
|
||||
collectionConfig: CollectionConfig
|
||||
localization?: any
|
||||
}): FormState => {
|
||||
const filename = getFilename()
|
||||
|
||||
const locales: string[] =
|
||||
localization?.localeCodes ||
|
||||
(localization?.locales
|
||||
? localization.locales.map((locale: { label: Record<string, string> | string } | string) =>
|
||||
typeof locale === 'string'
|
||||
? locale
|
||||
: typeof locale.label === 'string'
|
||||
? locale.label
|
||||
: '',
|
||||
)
|
||||
: []) ||
|
||||
[]
|
||||
|
||||
const columns = collectionConfig.fields
|
||||
.map((field) => ('name' in field ? field.name : null))
|
||||
.filter(Boolean)
|
||||
|
||||
return {
|
||||
name: {
|
||||
initialValue: filename,
|
||||
valid: true,
|
||||
value: filename,
|
||||
},
|
||||
columnsToExport: {
|
||||
initialValue: columns,
|
||||
valid: true,
|
||||
value: columns,
|
||||
},
|
||||
depth: {
|
||||
initialValue: 1,
|
||||
valid: true,
|
||||
value: 1,
|
||||
},
|
||||
drafts: {
|
||||
initialValue: 'false',
|
||||
valid: true,
|
||||
value: 'false',
|
||||
},
|
||||
format: {
|
||||
initialValue: 'csv',
|
||||
valid: true,
|
||||
value: 'csv',
|
||||
},
|
||||
limit: {
|
||||
initialValue: 100,
|
||||
valid: true,
|
||||
value: 100,
|
||||
},
|
||||
locales: {
|
||||
initialValue: locales,
|
||||
valid: true,
|
||||
value: locales,
|
||||
},
|
||||
selectionToUse: {
|
||||
initialValue: 'currentSelection',
|
||||
valid: true,
|
||||
value: 'currentSelection',
|
||||
},
|
||||
sort: {
|
||||
initialValue: ['ID'],
|
||||
valid: true,
|
||||
value: ['ID'],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
@layer payload-default {
|
||||
.export-drawer {
|
||||
&__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,153 @@
|
||||
'use client'
|
||||
import type { FormProps } from '@payloadcms/ui'
|
||||
import type { ClientField, CollectionConfig } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import {
|
||||
Collapsible,
|
||||
Drawer,
|
||||
Form,
|
||||
FormSubmit,
|
||||
PlusIcon,
|
||||
RenderFields,
|
||||
useConfig,
|
||||
useDocumentDrawer,
|
||||
useSelection,
|
||||
useServerFunctions,
|
||||
XIcon,
|
||||
} from '@payloadcms/ui'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { modifyFields } from '../../export/modifyFields.js'
|
||||
import { fields } from '../../exportFields.js'
|
||||
import { useInitialState } from './exportFields.js'
|
||||
|
||||
const baseClass = 'export-drawer'
|
||||
|
||||
export const ExportDrawer: React.FC<{
|
||||
collectionSlug: string
|
||||
drawerSlug: string
|
||||
exportCollectionSlug: string
|
||||
}> = ({ collectionSlug, drawerSlug, exportCollectionSlug }) => {
|
||||
const [DocumentDrawer, DocumentDrawerToggler, { isDrawerOpen, toggleDrawer }] = useDocumentDrawer(
|
||||
{
|
||||
collectionSlug: exportCollectionSlug,
|
||||
},
|
||||
)
|
||||
|
||||
const { toggleModal } = useModal()
|
||||
const { getFormState } = useServerFunctions()
|
||||
const [selectionToUse, setSelectionToUse] = React.useState('')
|
||||
const {
|
||||
config: { collections, localization },
|
||||
} = useConfig()
|
||||
|
||||
const collectionConfig =
|
||||
(collections.find((collection) => collection.slug === collectionSlug) as CollectionConfig) || {}
|
||||
|
||||
const collectionLabel = collectionConfig.labels
|
||||
? collectionConfig.labels.singular
|
||||
: collectionSlug || 'Collection'
|
||||
|
||||
const onSuccess = React.useCallback(() => {
|
||||
console.log('Exported')
|
||||
toggleModal(drawerSlug)
|
||||
}, [toggleModal, drawerSlug])
|
||||
|
||||
const exportFields = modifyFields(fields, collectionConfig) as ClientField[]
|
||||
const initialState = useInitialState({ collectionConfig, localization })
|
||||
|
||||
const onChange: FormProps['onChange'][0] = React.useCallback(
|
||||
(formData: any) => {
|
||||
const currentSelection = formData.formState.selectionToUse.value
|
||||
if (currentSelection !== selectionToUse) {
|
||||
setSelectionToUse(currentSelection)
|
||||
}
|
||||
console.log('formData', formData)
|
||||
},
|
||||
[setSelectionToUse],
|
||||
)
|
||||
|
||||
const onSubmit = React.useCallback(() => {
|
||||
console.log('Submit')
|
||||
}, [])
|
||||
|
||||
const selectedDocs = []
|
||||
const selection = useSelection()
|
||||
selection.selected.forEach((value, key) => {
|
||||
if (value === true) {
|
||||
selectedDocs.push(key)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DocumentDrawerToggler
|
||||
// className={[`${baseClass}__add-button`].filter(Boolean).join(' ')}
|
||||
// onClick={() => setShowTooltip(false)}
|
||||
// onMouseEnter={() => setShowTooltip(true)}
|
||||
// onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
Export
|
||||
</DocumentDrawerToggler>
|
||||
<DocumentDrawer
|
||||
|
||||
// onSave={onSave}
|
||||
/>
|
||||
</Fragment>
|
||||
// <Drawer
|
||||
// className={baseClass}
|
||||
// gutter={false}
|
||||
// Header={
|
||||
// <div className={`${baseClass}__header`}>
|
||||
// <h2>{`Export ${collectionLabel}`}</h2>
|
||||
// <button
|
||||
// className={`${baseClass}__close`}
|
||||
// onClick={() => toggleModal(drawerSlug)}
|
||||
// type="button"
|
||||
// >
|
||||
// <XIcon className={`${baseClass}__icon`} />
|
||||
// </button>
|
||||
// </div>
|
||||
// }
|
||||
// slug={drawerSlug}
|
||||
// >
|
||||
// <div className={`${baseClass}__subheader`}>
|
||||
// <div>Creating export{collectionLabel ? ` from ${collectionLabel}` : ''}</div>
|
||||
// <FormSubmit onClick={onSubmit}>Export</FormSubmit>
|
||||
// </div>
|
||||
// <div className={`${baseClass}__options`}>
|
||||
// <Collapsible header="Export options">
|
||||
// <Form
|
||||
// action={'/admin'}
|
||||
// initialState={initialState}
|
||||
// method="POST"
|
||||
// onChange={[onChange]}
|
||||
// onSuccess={onSuccess}
|
||||
// >
|
||||
// <RenderFields
|
||||
// fields={exportFields}
|
||||
// parentIndexPath=""
|
||||
// parentPath=""
|
||||
// parentSchemaPath=""
|
||||
// permissions={true}
|
||||
// />
|
||||
// </Form>
|
||||
// </Collapsible>
|
||||
// </div>
|
||||
// <div className={`${baseClass}__preview`}>
|
||||
// <div className={`${baseClass}__preview-title`}>
|
||||
// <h3>Preview</h3>
|
||||
// <span>(result count) total documents</span>
|
||||
// </div>
|
||||
// (data preview here)
|
||||
// {/* if selectionToUse is current selection, return only the selected docs */}
|
||||
// {/* if selectionToUse is all, return all docs and apply export settings */}
|
||||
// {/* if selectionToUse is current filters, add filters to all docs and export settings */}
|
||||
// </div>
|
||||
// </Drawer>
|
||||
)
|
||||
}
|
||||
117
packages/plugin-import-export/src/export/createExport.ts
Normal file
117
packages/plugin-import-export/src/export/createExport.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { PaginatedDocs, PayloadRequest, Sort, User, Where } from 'payload'
|
||||
|
||||
import { Buffer } from 'buffer'
|
||||
import { stringify } from 'csv-stringify/sync'
|
||||
import { APIError } from 'payload'
|
||||
|
||||
import { flattenObject } from './flattenObject.js'
|
||||
import { getFilename } from './getFilename.js'
|
||||
import { getSelect } from './getSelect.js'
|
||||
|
||||
type Export = {
|
||||
collections: {
|
||||
fields?: string[]
|
||||
slug: string
|
||||
sort: Sort
|
||||
where?: Where
|
||||
}[]
|
||||
exportsCollection: string
|
||||
format: 'csv' | 'json'
|
||||
globals?: string[]
|
||||
id: number | string
|
||||
name: string
|
||||
user: string
|
||||
userCollection: string
|
||||
}
|
||||
|
||||
export type CreateExportArgs = {
|
||||
input: Export
|
||||
req: PayloadRequest
|
||||
user?: User
|
||||
}
|
||||
|
||||
export const createExport = async (args: CreateExportArgs) => {
|
||||
const {
|
||||
input: { id, name: nameArg, collections = [], exportsCollection, format, user },
|
||||
req: { locale, payload },
|
||||
req,
|
||||
} = args
|
||||
|
||||
if (collections.length === 1) {
|
||||
const { slug, fields, sort, where } = collections[0] as Export['collections'][0]
|
||||
const collection = payload.config.collections.find((collection) => collection.slug === slug)
|
||||
if (!collection) {
|
||||
throw new Error(`Collection with slug ${slug} not found`)
|
||||
}
|
||||
const name = nameArg ?? `${getFilename()}-${collection.slug}`
|
||||
|
||||
if (!fields) {
|
||||
throw new APIError('fields must be defined when exporting')
|
||||
}
|
||||
|
||||
const findArgs = {
|
||||
collection: slug,
|
||||
depth: 0,
|
||||
limit: 100,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
page: 0,
|
||||
select: getSelect(fields),
|
||||
sort,
|
||||
user,
|
||||
where,
|
||||
}
|
||||
|
||||
let result: PaginatedDocs = { hasNextPage: true } as PaginatedDocs
|
||||
const outputData: string[] = []
|
||||
|
||||
let isFirstBatch = true
|
||||
|
||||
while (result.hasNextPage) {
|
||||
findArgs.page = findArgs.page + 1
|
||||
result = await payload.find(findArgs)
|
||||
|
||||
if (format === 'csv') {
|
||||
const csvInput = result.docs.map((doc) => flattenObject(doc))
|
||||
|
||||
const csvString = stringify(csvInput, {
|
||||
header: isFirstBatch, // Only include header in the first batch
|
||||
})
|
||||
|
||||
outputData.push(csvString)
|
||||
isFirstBatch = false
|
||||
}
|
||||
|
||||
if (format === 'json') {
|
||||
const jsonInput = result.docs.map((doc) => JSON.stringify(doc))
|
||||
outputData.push(jsonInput.join(',\n'))
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(
|
||||
format === 'json' ? `[${outputData.join(',')}]` : outputData.join(''),
|
||||
)
|
||||
|
||||
// when `disableJobsQueue` is true, the export is created synchronously in a beforeOperation hook
|
||||
if (!id) {
|
||||
req.file = {
|
||||
name: `${name}-${collection.slug}.${format}`,
|
||||
data: buffer,
|
||||
mimetype: format === 'json' ? 'application/json' : `text/${format}`,
|
||||
size: buffer.length,
|
||||
}
|
||||
} else {
|
||||
await req.payload.update({
|
||||
id,
|
||||
collection: exportsCollection,
|
||||
data: {},
|
||||
file: {
|
||||
name: `${name}.${format}`,
|
||||
data: buffer,
|
||||
mimetype: format === 'json' ? 'application/json' : `text/${format}`,
|
||||
size: buffer.length,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { TaskHandler, User } from 'payload'
|
||||
|
||||
import type { CreateExportArgs } from './createExport.js'
|
||||
|
||||
import { fields } from '../exportFields.js'
|
||||
import { createExport } from './createExport.js'
|
||||
|
||||
const inputSchema = fields.concat(
|
||||
{
|
||||
name: 'user',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'userCollection',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'exportsCollection',
|
||||
type: 'text',
|
||||
},
|
||||
)
|
||||
|
||||
export const createCollectionExportTask: TaskHandler<any, string> = {
|
||||
// @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',
|
||||
},
|
||||
],
|
||||
}
|
||||
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
|
||||
}
|
||||
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 [yyymmdd = '', hhmmss = ''] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
const formattedTime = (hhmmss.split('.')[0] ?? '').replace(/\D/g, '')
|
||||
|
||||
return `${formattedDate}_${formattedTime}`
|
||||
}
|
||||
27
packages/plugin-import-export/src/export/getSelect.ts
Normal file
27
packages/plugin-import-export/src/export/getSelect.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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) => {
|
||||
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
|
||||
}
|
||||
114
packages/plugin-import-export/src/export/modifyFields.ts
Normal file
114
packages/plugin-import-export/src/export/modifyFields.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { CollectionConfig, Field } from 'payload'
|
||||
|
||||
const fieldsToHide = ['slug', 'fields', 'where']
|
||||
|
||||
export function modifyFields(fields: Field[], collectionConfig: CollectionConfig): Field[] {
|
||||
const columns = collectionConfig.fields
|
||||
.map((field: Field) =>
|
||||
'name' in field
|
||||
? {
|
||||
label: field.name,
|
||||
value: field.name,
|
||||
}
|
||||
: null,
|
||||
)
|
||||
.filter(Boolean)
|
||||
|
||||
const fieldsToAdd = [
|
||||
{
|
||||
name: 'drafts',
|
||||
type: 'select',
|
||||
label: 'Drafts',
|
||||
options: [
|
||||
{
|
||||
label: 'True',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
label: 'False',
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'depth',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
label: 'Depth',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'selectionToUse',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{
|
||||
label: 'Use current selection',
|
||||
value: 'currentSelection',
|
||||
},
|
||||
{
|
||||
label: 'Use current filters',
|
||||
value: 'currentFilters',
|
||||
},
|
||||
{
|
||||
label: 'Use all documents',
|
||||
value: 'all',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'columnsToExport',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
label: 'Columns to Export',
|
||||
options: columns,
|
||||
},
|
||||
]
|
||||
|
||||
const flattenFields = (fields: Field[]): Field[] => {
|
||||
let result: Field[] = []
|
||||
|
||||
fields.forEach((field) => {
|
||||
if ('fields' in field && Array.isArray(field.fields)) {
|
||||
result = result.concat(flattenFields(field.fields))
|
||||
} else {
|
||||
if ('name' in field && !fieldsToHide.includes(field.name)) {
|
||||
result.push(field)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const modifiedFields = flattenFields(fields)
|
||||
|
||||
const allFields = [...modifiedFields, ...fieldsToAdd] as Field[]
|
||||
|
||||
const rows = [
|
||||
allFields[0],
|
||||
{
|
||||
type: 'row',
|
||||
fields: allFields.slice(1, 4).map((field) => ({
|
||||
...field,
|
||||
admin: {
|
||||
width: '33%',
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: allFields.slice(4, 7).map((field) => ({
|
||||
...field,
|
||||
admin: {
|
||||
width: '33%',
|
||||
},
|
||||
})),
|
||||
},
|
||||
]
|
||||
|
||||
if (allFields.length > 7) {
|
||||
rows.push(...allFields.slice(7))
|
||||
}
|
||||
|
||||
return [...rows] as Field[]
|
||||
}
|
||||
107
packages/plugin-import-export/src/export/recursiveAccessor.ts
Normal file
107
packages/plugin-import-export/src/export/recursiveAccessor.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export const recursiveAccessor = ({
|
||||
docRef,
|
||||
prefix = '',
|
||||
segment,
|
||||
}: {
|
||||
docRef: Record<string, unknown> | unknown
|
||||
prefix?: string
|
||||
segment: string
|
||||
}): { path: string; value: unknown } => {
|
||||
if (Array.isArray(docRef)) {
|
||||
docRef.forEach((item: Record<string, unknown>, i) => {
|
||||
Object.entries(item).forEach(([itemKey, itemValue]) => {
|
||||
recursiveAccessor({
|
||||
docRef: itemValue as Record<string, unknown>,
|
||||
prefix: `${prefix}_${i}_${itemKey}`,
|
||||
segment: itemKey,
|
||||
})
|
||||
})
|
||||
})
|
||||
} else if (typeof docRef === 'object') {
|
||||
Object.entries(docRef as Record<string, unknown>).forEach(([key, value]) => {
|
||||
if (value && typeof value === 'object') {
|
||||
recursiveAccessor({
|
||||
docRef: value as Record<string, unknown>,
|
||||
prefix: `${prefix}_${key}`,
|
||||
segment: key,
|
||||
})
|
||||
} else {
|
||||
return { path: `${prefix ? `${prefix}_` : ''}${key}`, value }
|
||||
// data[`${key}`] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
return { path: `${prefix ? `${prefix}_` : ''}${segment}`, value: docRef }
|
||||
}
|
||||
|
||||
// export const recursiveAccessor = ({
|
||||
// docRef,
|
||||
// prefix = '',
|
||||
// segment,
|
||||
// }: {
|
||||
// docRef: Record<string, unknown> | unknown
|
||||
// prefix?: string
|
||||
// segment: string
|
||||
// }): { path: string; value: unknown } => {
|
||||
// if (Array.isArray(docRef)) {
|
||||
// docRef.forEach((item: Record<string, unknown>, i) => {
|
||||
// Object.entries(item).forEach(([itemKey, itemValue]) => {
|
||||
// recursiveAccessor({
|
||||
// docRef: itemValue as Record<string, unknown>,
|
||||
// prefix: `${prefix}_${i}_${itemKey}`,
|
||||
// segment: itemKey,
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// } else if (typeof docRef === 'object') {
|
||||
// Object.entries(docRef as Record<string, unknown>).forEach(([key, value]) => {
|
||||
// if (value && typeof value === 'object') {
|
||||
// recursiveAccessor({
|
||||
// docRef: value as Record<string, unknown>,
|
||||
// prefix: `${prefix}_${key}`,
|
||||
// segment: key,
|
||||
// })
|
||||
// } else {
|
||||
// return { path: `${prefix ? `${prefix}_` : ''}${key}`, value }
|
||||
// // data[`${key}`] = value
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// return { path: `${prefix ? `${prefix}_` : ''}${segment}`, value: docRef }
|
||||
// }
|
||||
|
||||
// const traverseFieldsCallback: TraverseFieldsCallback<Record<string, unknown>> = ({
|
||||
// field,
|
||||
// parentRef,
|
||||
// ref,
|
||||
// }) => {
|
||||
// // always false because we are using flattenedFields but useful for type narrowing
|
||||
// if (!('name' in field)) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if (field.type === 'group') {
|
||||
// selectRef = select[field.name] = {}
|
||||
// ref._name = field.name
|
||||
// }
|
||||
// const prefix =
|
||||
// typeof parentRef === 'object' && parentRef?._name && typeof parentRef._name === 'string'
|
||||
// ? `${parentRef._name}.`
|
||||
// : ''
|
||||
//
|
||||
// // when fields aren't defined we assume all fields are included
|
||||
// if (
|
||||
// fields &&
|
||||
// 'name' in field &&
|
||||
// !fields.some((f) => {
|
||||
// const segment = f.indexOf('.') > -1 ? f.substring(0, f.indexOf('.')) : f
|
||||
// return segment === `${prefix}${field.name}`
|
||||
// })
|
||||
// ) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if (field.name) {
|
||||
// selectRef[field.name] = true
|
||||
// }
|
||||
// }
|
||||
74
packages/plugin-import-export/src/exportFields.ts
Normal file
74
packages/plugin-import-export/src/exportFields.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { getFilename } from './export/getFilename.js'
|
||||
|
||||
const exportCollectionFields: Field[] = [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'fields',
|
||||
type: 'text',
|
||||
hasMany: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
label: 'Limit',
|
||||
},
|
||||
{
|
||||
name: 'sort',
|
||||
type: 'text',
|
||||
hasMany: true,
|
||||
label: 'Sort By',
|
||||
},
|
||||
{
|
||||
name: 'where',
|
||||
type: 'json',
|
||||
},
|
||||
]
|
||||
|
||||
export const fields: Field[] = [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
defaultValue: () => getFilename(),
|
||||
label: 'File Name',
|
||||
virtual: true,
|
||||
},
|
||||
{
|
||||
name: 'format',
|
||||
type: 'select',
|
||||
label: 'Export Format',
|
||||
options: [
|
||||
{
|
||||
label: 'JSON',
|
||||
value: 'json',
|
||||
},
|
||||
{
|
||||
label: 'CSV',
|
||||
value: 'csv',
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'collections',
|
||||
type: 'array',
|
||||
fields: exportCollectionFields,
|
||||
},
|
||||
{
|
||||
name: 'locales',
|
||||
type: 'text',
|
||||
hasMany: true,
|
||||
label: 'Locales',
|
||||
},
|
||||
// {
|
||||
// name: 'globals',
|
||||
// type: 'text',
|
||||
// hasMany: true,
|
||||
// },
|
||||
]
|
||||
1
packages/plugin-import-export/src/exports/rsc.ts
Normal file
1
packages/plugin-import-export/src/exports/rsc.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ExportButton } from '../components/ExportButton/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'
|
||||
81
packages/plugin-import-export/src/getExportCollection.ts
Normal file
81
packages/plugin-import-export/src/getExportCollection.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type {
|
||||
CollectionAfterChangeHook,
|
||||
CollectionBeforeOperationHook,
|
||||
CollectionConfig,
|
||||
} from 'payload'
|
||||
|
||||
import type { CollectionOverride, ImportExportPluginConfig } from './types.js'
|
||||
|
||||
import { createExport } from './export/createExport.js'
|
||||
import { fields } from './exportFields.js'
|
||||
|
||||
export const getExportCollection = ({
|
||||
pluginConfig,
|
||||
}: {
|
||||
pluginConfig: ImportExportPluginConfig
|
||||
}): CollectionConfig => {
|
||||
const { overrideExportCollection } = pluginConfig
|
||||
|
||||
const beforeOperation: CollectionBeforeOperationHook[] = []
|
||||
const afterChange: CollectionAfterChangeHook[] = []
|
||||
|
||||
let collection: CollectionOverride = {
|
||||
slug: 'exports',
|
||||
access: {
|
||||
update: () => false,
|
||||
},
|
||||
admin: {
|
||||
group: false,
|
||||
useAsTitle: 'filename',
|
||||
},
|
||||
disableDuplicate: true,
|
||||
fields,
|
||||
hooks: {
|
||||
afterChange,
|
||||
beforeOperation,
|
||||
},
|
||||
upload: {
|
||||
filesRequiredOnCreate: false,
|
||||
// must be csv, json or zip
|
||||
mimeTypes: ['application/json', 'text/csv', 'application/zip'],
|
||||
},
|
||||
}
|
||||
|
||||
if (typeof overrideExportCollection === 'function') {
|
||||
collection = overrideExportCollection(collection)
|
||||
}
|
||||
|
||||
if (pluginConfig.disableJobsQueue) {
|
||||
beforeOperation.push(async ({ args, operation, req }) => {
|
||||
if (operation !== 'create') {
|
||||
return
|
||||
}
|
||||
const { user } = req
|
||||
if (args.data.collections.length === 1) {
|
||||
await createExport({ input: { ...args.data, user }, req })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
afterChange.push(async ({ doc, operation, req }) => {
|
||||
if (operation !== 'create') {
|
||||
return
|
||||
}
|
||||
|
||||
if (doc.collections.length === 1) {
|
||||
const input = {
|
||||
...doc,
|
||||
exportsCollection: collection.slug,
|
||||
user: req?.user?.id || req?.user?.user?.id,
|
||||
userCollection: 'users',
|
||||
}
|
||||
const { id } = await req.payload.jobs.queue({
|
||||
input,
|
||||
task: 'createCollectionExport',
|
||||
})
|
||||
void req.payload.jobs.runByID({ id })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
59
packages/plugin-import-export/src/index.ts
Normal file
59
packages/plugin-import-export/src/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Config, JobsConfig } from 'payload'
|
||||
|
||||
import { deepMergeSimple } from 'payload'
|
||||
|
||||
import type { ImportExportPluginConfig } from './types.js'
|
||||
|
||||
import { createCollectionExportTask } from './export/createExportCollectionTask.js'
|
||||
import { getExportCollection } from './getExportCollection.js'
|
||||
import { translations } from './translations/index.js'
|
||||
|
||||
export const importExportPlugin =
|
||||
(pluginConfig: ImportExportPluginConfig) =>
|
||||
(config: Config): Config => {
|
||||
const exportCollection = getExportCollection({ pluginConfig })
|
||||
if (config.collections) {
|
||||
config.collections.push(exportCollection)
|
||||
} else {
|
||||
config.collections = [exportCollection]
|
||||
}
|
||||
|
||||
// inject the createExport job into the config
|
||||
config.jobs =
|
||||
config.jobs ||
|
||||
({
|
||||
tasks: [createCollectionExportTask],
|
||||
} 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: { afterListControls: [] } }
|
||||
}
|
||||
if (!collection.admin.components) {
|
||||
collection.admin.components = { afterListControls: [] }
|
||||
}
|
||||
if (!collection.admin.components.afterListControls) {
|
||||
collection.admin.components.afterListControls = []
|
||||
}
|
||||
collection.admin.components.afterListControls.push({
|
||||
clientProps: {
|
||||
exportCollectionSlug: exportCollection.slug,
|
||||
},
|
||||
path: '@payloadcms/plugin-import-export/rsc#ExportButton',
|
||||
})
|
||||
})
|
||||
|
||||
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"]
|
||||
}
|
||||
28
packages/plugin-import-export/src/types.ts
Normal file
28
packages/plugin-import-export/src/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { CollectionConfig, UploadConfig } from 'payload'
|
||||
|
||||
export type CollectionOverride = {
|
||||
admin: CollectionConfig['admin']
|
||||
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
|
||||
/**
|
||||
* Globals to include the Import/Export controls in
|
||||
*/
|
||||
globals?: string[]
|
||||
/**
|
||||
* 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"}]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { FormState } from 'payload'
|
||||
|
||||
export const initialState: FormState = {
|
||||
depth: {
|
||||
initialValue: 1,
|
||||
valid: true,
|
||||
value: 1,
|
||||
},
|
||||
drafts: {
|
||||
initialValue: 'include',
|
||||
valid: true,
|
||||
value: 'include',
|
||||
},
|
||||
filename: {
|
||||
initialValue: 'untitled-collection-export-01-01-2025.csv',
|
||||
valid: true,
|
||||
value: 'untitled-collection-export-01-01-2025.csv',
|
||||
},
|
||||
format: {
|
||||
initialValue: 'csv',
|
||||
valid: true,
|
||||
value: 'csv',
|
||||
},
|
||||
limit: {
|
||||
initialValue: 100,
|
||||
valid: true,
|
||||
value: 100,
|
||||
},
|
||||
locales: {
|
||||
initialValue: ['all'],
|
||||
valid: true,
|
||||
value: ['all'],
|
||||
},
|
||||
sortby: {
|
||||
initialValue: ['ID'],
|
||||
valid: true,
|
||||
value: ['ID'],
|
||||
},
|
||||
useCurrentFilters: {
|
||||
initialValue: false,
|
||||
valid: true,
|
||||
value: false,
|
||||
},
|
||||
useCurrentSelection: {
|
||||
initialValue: false,
|
||||
valid: true,
|
||||
value: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
@import '../../../scss/styles';
|
||||
|
||||
@layer payload-default {
|
||||
.export-drawer {
|
||||
&__subheader {
|
||||
padding: 0 var(--gutter-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--theme-border-color);
|
||||
}
|
||||
|
||||
&__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);
|
||||
}
|
||||
}
|
||||
}
|
||||
203
packages/ui/src/elements/ListControls/ExportDrawer/index.tsx
Normal file
203
packages/ui/src/elements/ListControls/ExportDrawer/index.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig, ClientField } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { Collapsible } from '../../../elements/Collapsible/index.js'
|
||||
import { Form } from '../../../forms/Form/index.js'
|
||||
import { RenderFields } from '../../../forms/RenderFields/index.js'
|
||||
import { FormSubmit } from '../../../forms/Submit/index.js'
|
||||
import { useConfig } from '../../../providers/Config/index.js'
|
||||
import { DrawerHeader } from '../../BulkUpload/Header/index.js'
|
||||
import { Drawer } from '../../Drawer/index.js'
|
||||
import { initialState } from './exportFields.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'export-drawer'
|
||||
|
||||
export const ExportDrawer: React.FC<{
|
||||
collectionConfig: ClientCollectionConfig
|
||||
collectionLabel: string | undefined
|
||||
drawerSlug: string
|
||||
}> = ({ collectionConfig, collectionLabel, drawerSlug }) => {
|
||||
const { toggleModal } = useModal()
|
||||
const {
|
||||
config: { localization },
|
||||
} = useConfig()
|
||||
const localizationEnabled = localization && localization.locales.length > 1
|
||||
const localeOptions =
|
||||
localizationEnabled &&
|
||||
localization.locales.map((locale) =>
|
||||
typeof locale === 'string'
|
||||
? { label: locale, value: locale }
|
||||
: { label: locale.label, value: locale.code },
|
||||
)
|
||||
|
||||
const sortByFields = collectionConfig.fields.filter((field) => fieldAffectsData(field))
|
||||
const sortByOptions = sortByFields.map((field) => ({
|
||||
label: field.label || field.name,
|
||||
value: field.name,
|
||||
}))
|
||||
|
||||
const exportFields: ClientField[] = [
|
||||
{
|
||||
name: 'filename',
|
||||
type: 'text',
|
||||
label: 'Filename',
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'format',
|
||||
type: 'select',
|
||||
admin: {
|
||||
width: '33.3%',
|
||||
},
|
||||
label: 'Export Format',
|
||||
options: [
|
||||
{
|
||||
label: 'CSV',
|
||||
value: 'csv',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
admin: {
|
||||
width: '33.3%',
|
||||
},
|
||||
label: 'Limit',
|
||||
},
|
||||
{
|
||||
name: 'sortby',
|
||||
type: 'select',
|
||||
admin: {
|
||||
isClearable: false,
|
||||
width: '33.3%',
|
||||
},
|
||||
label: 'Sort By',
|
||||
options: sortByOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
localizationEnabled && {
|
||||
name: 'locales',
|
||||
type: 'select',
|
||||
admin: {
|
||||
width: '33.3%',
|
||||
},
|
||||
hasMany: true,
|
||||
label: 'Locales',
|
||||
options: [
|
||||
{
|
||||
label: 'All',
|
||||
value: 'all',
|
||||
},
|
||||
...localeOptions,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'drafts',
|
||||
type: 'select',
|
||||
admin: {
|
||||
width: `${localizationEnabled ? '33.3%' : '50%'}`,
|
||||
},
|
||||
label: 'Drafts',
|
||||
options: [
|
||||
{
|
||||
label: 'Include',
|
||||
value: 'include',
|
||||
},
|
||||
{
|
||||
label: 'Exclude',
|
||||
value: 'exclude',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'depth',
|
||||
type: 'number',
|
||||
admin: {
|
||||
width: `${localizationEnabled ? '33.3%' : '50%'}`,
|
||||
},
|
||||
label: 'Depth',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'useCurrentFilters',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
width: 'min-content',
|
||||
},
|
||||
label: 'Use current selection',
|
||||
},
|
||||
{
|
||||
name: 'useCurrentSelection',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
width: 'min-content',
|
||||
},
|
||||
label: 'Use selected documents',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const onSuccess = React.useCallback(() => {
|
||||
console.log('Exported')
|
||||
toggleModal(drawerSlug)
|
||||
}, [toggleModal, drawerSlug])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={baseClass}
|
||||
gutter={false}
|
||||
Header={
|
||||
<DrawerHeader
|
||||
onClose={() => {
|
||||
toggleModal(drawerSlug)
|
||||
}}
|
||||
title={`Export ${collectionLabel}`}
|
||||
/>
|
||||
}
|
||||
slug={drawerSlug}
|
||||
>
|
||||
<div className={`${baseClass}__subheader`}>
|
||||
<div>Creating export{collectionLabel ? ` from ${collectionLabel}` : ''}</div>
|
||||
<FormSubmit>Export</FormSubmit>
|
||||
</div>
|
||||
<div className={`${baseClass}__options`}>
|
||||
<Collapsible header="Export options">
|
||||
<Form action={'/admin'} initialState={initialState} method="POST" onSuccess={onSuccess}>
|
||||
<RenderFields
|
||||
fields={exportFields}
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath=""
|
||||
permissions={true}
|
||||
/>
|
||||
</Form>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<div className={`${baseClass}__preview`}>
|
||||
<div className={`${baseClass}__preview-title`}>
|
||||
<h3>Preview</h3>
|
||||
<span>(result count) total documents</span>
|
||||
</div>
|
||||
(data preview here)
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig, Where } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { useWindowInfo } from '@faceless-ui/window-info'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Popup, PopupList } from '../../elements/Popup/index.js'
|
||||
import { useUseTitleField } from '../../hooks/useUseAsTitle.js'
|
||||
import { ChevronIcon } from '../../icons/Chevron/index.js'
|
||||
import { Dots } from '../../icons/Dots/index.js'
|
||||
import { SearchIcon } from '../../icons/Search/index.js'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
@@ -19,13 +22,14 @@ import { PublishMany } from '../PublishMany/index.js'
|
||||
import { SearchFilter } from '../SearchFilter/index.js'
|
||||
import { UnpublishMany } from '../UnpublishMany/index.js'
|
||||
import { WhereBuilder } from '../WhereBuilder/index.js'
|
||||
import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js'
|
||||
import './index.scss'
|
||||
import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js'
|
||||
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
|
||||
|
||||
const baseClass = 'list-controls'
|
||||
|
||||
export type ListControlsProps = {
|
||||
readonly afterListControls?: React.ReactNode | React.ReactNode[]
|
||||
readonly beforeActions?: React.ReactNode[]
|
||||
readonly collectionConfig: ClientCollectionConfig
|
||||
readonly collectionSlug: string
|
||||
@@ -46,6 +50,7 @@ export type ListControlsProps = {
|
||||
*/
|
||||
export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
const {
|
||||
afterListControls,
|
||||
beforeActions,
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
@@ -56,13 +61,17 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
renderedFilters,
|
||||
} = props
|
||||
|
||||
const collectionLabel = (collectionConfig.labels?.plural as string) || collectionSlug || ''
|
||||
const { handleSearchChange, query } = useListQuery()
|
||||
const titleField = useUseTitleField(collectionConfig)
|
||||
const { i18n, t } = useTranslation()
|
||||
const { toggleModal } = useModal()
|
||||
const {
|
||||
breakpoints: { s: smallBreak },
|
||||
} = useWindowInfo()
|
||||
|
||||
const exportDrawerSlug = `export-drawer-${collectionSlug}`
|
||||
|
||||
const searchLabel =
|
||||
(titleField &&
|
||||
getTranslation(
|
||||
@@ -193,6 +202,25 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
{t('general:sort')}
|
||||
</Pill>
|
||||
)}
|
||||
{afterListControls != null && afterListControls !== false && (
|
||||
<Popup
|
||||
button={<Dots />}
|
||||
className={`${baseClass}__popup`}
|
||||
horizontalAlign="right"
|
||||
size="large"
|
||||
verticalAlign="bottom"
|
||||
>
|
||||
<PopupList.ButtonGroup>
|
||||
{Array.isArray(afterListControls) ? (
|
||||
afterListControls.map((control, index) => (
|
||||
<PopupList.Button key={index}>{control}</PopupList.Button>
|
||||
))
|
||||
) : (
|
||||
<PopupList.Button>{afterListControls}</PopupList.Button>
|
||||
)}
|
||||
</PopupList.ButtonGroup>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
27
packages/ui/src/icons/Dots/index.scss
Normal file
27
packages/ui/src/icons/Dots/index.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
@import '../../scss/styles';
|
||||
|
||||
@layer payload-default {
|
||||
.dots {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
background-color: var(--theme-elevation-150);
|
||||
border-radius: $style-radius-m;
|
||||
height: calc(var(--base) * 1.2);
|
||||
width: calc(var(--base) * 1.2);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
> div {
|
||||
width: 2.5px;
|
||||
height: 2.5px;
|
||||
border-radius: 100%;
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/ui/src/icons/Dots/index.tsx
Normal file
11
packages/ui/src/icons/Dots/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
export const Dots: React.FC<{ className?: string }> = ({ className }) => (
|
||||
<div className={[className && className, 'dots'].filter(Boolean).join(' ')}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
)
|
||||
@@ -44,6 +44,7 @@ const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.
|
||||
|
||||
export type ListViewSlots = {
|
||||
AfterList?: React.ReactNode
|
||||
AfterListControls?: React.ReactNode | React.ReactNode[]
|
||||
AfterListTable?: React.ReactNode
|
||||
BeforeList?: React.ReactNode
|
||||
BeforeListTable?: React.ReactNode
|
||||
@@ -68,6 +69,7 @@ export type ListViewClientProps = {
|
||||
export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
|
||||
const {
|
||||
AfterList,
|
||||
AfterListControls,
|
||||
AfterListTable,
|
||||
beforeActions,
|
||||
BeforeList,
|
||||
@@ -207,6 +209,7 @@ export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
|
||||
t={t}
|
||||
/>
|
||||
<ListControls
|
||||
afterListControls={AfterListControls}
|
||||
beforeActions={
|
||||
enableRowSelections && typeof onBulkSelect === 'function'
|
||||
? beforeActions
|
||||
|
||||
67
pnpm-lock.yaml
generated
67
pnpm-lock.yaml
generated
@@ -45,7 +45,7 @@ importers:
|
||||
version: 1.50.0
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
'@sentry/node':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1
|
||||
@@ -135,7 +135,7 @@ importers:
|
||||
version: 10.1.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
|
||||
next:
|
||||
specifier: 15.1.5
|
||||
version: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
version: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
open:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
@@ -1004,11 +1004,36 @@ 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
|
||||
devDependencies:
|
||||
'@payloadcms/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config
|
||||
payload:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
|
||||
packages/plugin-multi-tenant:
|
||||
dependencies:
|
||||
next:
|
||||
specifier: ^15.0.3
|
||||
version: 15.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
version: 15.1.3(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
devDependencies:
|
||||
'@payloadcms/eslint-config':
|
||||
specifier: workspace:*
|
||||
@@ -1070,7 +1095,7 @@ importers:
|
||||
dependencies:
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
'@sentry/types':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1
|
||||
@@ -1420,7 +1445,7 @@ importers:
|
||||
version: link:../plugin-cloud-storage
|
||||
uploadthing:
|
||||
specifier: 7.3.0
|
||||
version: 7.3.0(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))
|
||||
version: 7.3.0(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))
|
||||
devDependencies:
|
||||
payload:
|
||||
specifier: workspace:*
|
||||
@@ -1644,6 +1669,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
|
||||
@@ -1694,7 +1722,7 @@ importers:
|
||||
version: link:../packages/ui
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
'@sentry/react':
|
||||
specifier: ^7.77.0
|
||||
version: 7.119.2(react@19.0.0)
|
||||
@@ -1716,6 +1744,9 @@ importers:
|
||||
create-payload-app:
|
||||
specifier: workspace:*
|
||||
version: link:../packages/create-payload-app
|
||||
csv-parse:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
dotenv:
|
||||
specifier: 16.4.7
|
||||
version: 16.4.7
|
||||
@@ -1745,7 +1776,7 @@ importers:
|
||||
version: 8.9.5(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
|
||||
next:
|
||||
specifier: 15.1.5
|
||||
version: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
version: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
nodemailer:
|
||||
specifier: 6.9.16
|
||||
version: 6.9.16
|
||||
@@ -6106,6 +6137,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==}
|
||||
|
||||
@@ -13595,7 +13632,7 @@ snapshots:
|
||||
'@sentry/utils': 7.119.2
|
||||
localforage: 1.10.0
|
||||
|
||||
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))':
|
||||
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
|
||||
@@ -13611,7 +13648,7 @@ snapshots:
|
||||
'@sentry/vercel-edge': 8.37.1
|
||||
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
chalk: 3.0.0
|
||||
next: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
next: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
resolve: 1.22.8
|
||||
rollup: 3.29.5
|
||||
stacktrace-parser: 0.1.10
|
||||
@@ -15469,6 +15506,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: {}
|
||||
@@ -18054,7 +18095,7 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@15.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
next@15.1.3(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
dependencies:
|
||||
'@next/env': 15.1.3
|
||||
'@swc/counter': 0.1.3
|
||||
@@ -18082,7 +18123,7 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
dependencies:
|
||||
'@next/env': 15.1.5
|
||||
'@swc/counter': 0.1.3
|
||||
@@ -19749,14 +19790,14 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
uploadthing@7.3.0(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)):
|
||||
uploadthing@7.3.0(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)):
|
||||
dependencies:
|
||||
'@effect/platform': 0.69.8(effect@3.10.3)
|
||||
'@uploadthing/mime-types': 0.3.2
|
||||
'@uploadthing/shared': 7.1.1
|
||||
effect: 3.10.3
|
||||
optionalDependencies:
|
||||
next: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
next: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
|
||||
@@ -33,6 +33,26 @@ export const Posts: CollectionConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
afterListControls: [
|
||||
{
|
||||
path: '/components/Banner/index.js#Banner',
|
||||
clientProps: {
|
||||
message: 'AfterListControls',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/components/Banner/index.js#Banner',
|
||||
clientProps: {
|
||||
message: 'Many of them',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/components/Banner/index.js#Banner',
|
||||
clientProps: {
|
||||
message: 'Ok last one',
|
||||
},
|
||||
},
|
||||
],
|
||||
afterList: [
|
||||
{
|
||||
path: '/components/Banner/index.js#Banner',
|
||||
|
||||
@@ -196,6 +196,19 @@ describe('List View', () => {
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should render custom afterListControls component', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
const kebabMenu = page.locator('.list-controls__popup')
|
||||
await expect(kebabMenu).toBeVisible()
|
||||
await kebabMenu.click()
|
||||
|
||||
await expect(
|
||||
page.locator('.popup-button-list__button').locator('div', {
|
||||
hasText: 'AfterListControls',
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should render custom afterListTable component', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
await expect(
|
||||
|
||||
@@ -40,6 +40,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:*",
|
||||
@@ -64,6 +65,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",
|
||||
"dotenv": "16.4.7",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"eslint-plugin-playwright": "2.2.0",
|
||||
|
||||
1
test/plugin-import-export/.gitignore
vendored
Normal file
1
test/plugin-import-export/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
uploads
|
||||
112
test/plugin-import-export/collections/Pages.ts
Normal file
112
test/plugin-import-export/collections/Pages.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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: '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
|
||||
],
|
||||
}
|
||||
60
test/plugin-import-export/config.ts
Normal file
60
test/plugin-import-export/config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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 { devUser } from '../credentials.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 payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
502
test/plugin-import-export/int.spec.ts
Normal file
502
test/plugin-import-export/int.spec.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
import type { CollectionSlug, Payload, User } from 'payload'
|
||||
import fs from 'fs'
|
||||
import { parse } from 'csv-parse'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Page } from './payload-types.js'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { richTextData } from './seed/richTextData.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
|
||||
let payload: Payload
|
||||
let page: Page
|
||||
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))
|
||||
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 () => {
|
||||
// large data set
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Page ${i}`,
|
||||
group: {
|
||||
array: [{ field1: 'test' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
sort: 'createdAt',
|
||||
fields: [
|
||||
'id',
|
||||
'title',
|
||||
'group.value',
|
||||
'group.array.field1',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
],
|
||||
},
|
||||
],
|
||||
format: 'csv',
|
||||
},
|
||||
})
|
||||
|
||||
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('Page 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 array', async () => {
|
||||
// large data set
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: ['id', 'array'],
|
||||
},
|
||||
],
|
||||
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].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 () => {
|
||||
// large data set
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: ['id', 'array.field1'],
|
||||
},
|
||||
],
|
||||
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].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 () => {
|
||||
// large data set
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Array ${i}`,
|
||||
hasManyNumber: [0, 1, 1, 2, 3, 5, 8, 13, 21],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: ['id', 'hasManyNumber'],
|
||||
},
|
||||
],
|
||||
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].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 () => {
|
||||
// large data set
|
||||
const allPromises = []
|
||||
let promises = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
promises.push(
|
||||
payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Array ${i}`,
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'hero',
|
||||
title: 'test',
|
||||
},
|
||||
{
|
||||
blockType: 'content',
|
||||
richText: richTextData,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: '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')
|
||||
})
|
||||
|
||||
it('should create a file for collection csv using a where filter', async () => {
|
||||
// data set
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Array ${i}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: ['id', 'title'],
|
||||
where: {
|
||||
title: { equals: 'Array 2' },
|
||||
},
|
||||
},
|
||||
],
|
||||
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].title).toStrictEqual('Array 2')
|
||||
})
|
||||
|
||||
it('should create a JSON file for collection', async () => {
|
||||
// data set
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: `Array ${i}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let doc = await payload.create({
|
||||
collection: 'exports-tasks' as CollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: ['id', 'title'],
|
||||
},
|
||||
],
|
||||
format: 'json',
|
||||
},
|
||||
})
|
||||
|
||||
// Wait for job to complete...
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
doc = await payload.findByID({
|
||||
collection: 'exports-tasks' as CollectionSlug,
|
||||
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('Array 0')
|
||||
})
|
||||
|
||||
it('should create jobs task for exports', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports-tasks' as CollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
fields: ['id', 'title'],
|
||||
},
|
||||
],
|
||||
format: 'csv',
|
||||
},
|
||||
})
|
||||
|
||||
const jobs = await payload.find({
|
||||
collection: 'payload-jobs' as CollectionSlug,
|
||||
})
|
||||
|
||||
expect(jobs.docs).toHaveLength(1)
|
||||
})
|
||||
|
||||
// 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({
|
||||
collection: '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: {
|
||||
collections: [
|
||||
{
|
||||
slug: '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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 = JSON.parse(str)
|
||||
return json
|
||||
} catch (error) {
|
||||
console.error('Error reading JSON file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
551
test/plugin-import-export/payload-types.ts
Normal file
551
test/plugin-import-export/payload-types.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
users: User;
|
||||
pages: Page;
|
||||
'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-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;
|
||||
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-tasks".
|
||||
*/
|
||||
export interface ExportsTask {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
format: 'json' | 'csv';
|
||||
collections?:
|
||||
| {
|
||||
slug: string;
|
||||
fields?: string[] | null;
|
||||
limit?: number | null;
|
||||
sort?: string[] | null;
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
locales?: string[] | 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-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;
|
||||
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-tasks_select".
|
||||
*/
|
||||
export interface ExportsTasksSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
format?: T;
|
||||
collections?:
|
||||
| T
|
||||
| {
|
||||
slug?: T;
|
||||
fields?: T;
|
||||
limit?: T;
|
||||
sort?: T;
|
||||
where?: T;
|
||||
id?: T;
|
||||
};
|
||||
locales?: 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: 'json' | 'csv';
|
||||
collections?:
|
||||
| {
|
||||
slug: string;
|
||||
fields?: string[] | null;
|
||||
limit?: number | null;
|
||||
sort?: string[] | null;
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
}[]
|
||||
| null;
|
||||
locales?: string[] | null;
|
||||
user?: string | null;
|
||||
userCollection?: 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 {}
|
||||
}
|
||||
22
test/plugin-import-export/seed/index.ts
Normal file
22
test/plugin-import-export/seed/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
|
||||
export const seed = async (payload: Payload): Promise<boolean> => {
|
||||
payload.logger.info('Seeding data...')
|
||||
const req = {} as PayloadRequest
|
||||
|
||||
try {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
req,
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
99
test/plugin-import-export/seed/richTextData.ts
Normal file
99
test/plugin-import-export/seed/richTextData.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
export const richTextData = [
|
||||
{
|
||||
type: 'ul',
|
||||
children: [
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: 'I am semantically connected to my sub-bullets',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'ul',
|
||||
children: [
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
text: 'I am sub-bullets that are semantically connected to the parent bullet',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: 'Normal bullet',
|
||||
},
|
||||
],
|
||||
type: 'li',
|
||||
},
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
type: 'ul',
|
||||
children: [
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
text: 'I am the old style of sub-bullet',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
text: 'Another normal bullet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: 'This text precedes a nested list',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'ul',
|
||||
children: [
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
text: 'I am a sub-bullet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'li',
|
||||
children: [
|
||||
{
|
||||
text: 'And I am another sub-bullet',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
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/plugin-search/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"
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
{
|
||||
"path": "./packages/plugin-seo"
|
||||
},
|
||||
{
|
||||
"path": "./packages/plugin-import-export"
|
||||
},
|
||||
{
|
||||
"path": "./packages/plugin-stripe"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user