Compare commits

..

41 Commits

Author SHA1 Message Date
Kendell Joseph
ebfc4b5936 chore: WIP changes 2025-02-06 16:00:12 -05:00
Kendell Joseph
fb90315307 chore: adds exportsCollection to input to support export collection slug overrides 2025-02-06 13:57:36 -05:00
Kendell Joseph
2418d56824 chore: corrects type warning 2025-02-06 13:57:05 -05:00
Kendell Joseph
5ea3c276c1 chore: implements export to JSON format 2025-02-06 13:55:38 -05:00
Kendell Joseph
3f12fdfa8c chore: sends the exports collection slug to the task 2025-02-06 13:52:44 -05:00
Kendell Joseph
2866a2d006 chore: adds test for JSON export format 2025-02-06 13:51:24 -05:00
Jessica Chowdhury
be6afb5e8f chore: make selectionToUse into state 2025-02-06 13:56:10 +00:00
Jessica Chowdhury
83eb2cebdb chore: updates fields in export drawer 2025-02-06 12:24:22 +00:00
Kendell Joseph
3fb057a2dc chore: adds where clause test 2025-02-05 13:13:43 -05:00
Kendell Joseph
93062f6e37 chore: adds where clause option 2025-02-05 13:11:46 -05:00
Kendell Joseph
b924ca9f5f chore: updates Export type 2025-02-05 13:07:36 -05:00
Jessica Chowdhury
3a420a6e4e chore: fix pnpm lock file 2025-02-05 15:12:13 +00:00
Jessica Chowdhury
66f8029873 Merge branch 'main' into feat/export-ui 2025-02-05 15:11:35 +00:00
Jessica Chowdhury
4368560b0d chore: use export collection fields in export drawer 2025-02-05 14:57:04 +00:00
Jessica Chowdhury
88ee2aab04 chore: merge with main 2025-02-05 13:06:10 +00:00
Dan Ribbens
094157aaf0 feat: jobs queue flag 2025-02-04 17:12:15 -05:00
Jessica Chowdhury
868daeb03b chore(ui): fix conditional check in listControls 2025-02-04 17:41:38 +00:00
Jessica Chowdhury
66d69cbb8f chore(ui): renders kebab menu containing admin.components.afterListControls 2025-02-04 17:06:43 +00:00
Jessica Chowdhury
b67bea0011 feat(ui): adds admin.components.afterListControls option 2025-02-04 16:38:26 +00:00
Jessica Chowdhury
a8ef7869a4 chore: add export drawer and update tsconfig 2025-02-04 16:04:44 +00:00
Jessica Chowdhury
4bbf6ab193 chore: move export button into plugin and misc updates 2025-02-04 14:19:26 +00:00
Dan Ribbens
1b145ff28f feat: exports file names 2025-02-03 16:15:43 -05:00
Dan Ribbens
7fe74f0c67 test: blocks 2025-02-03 16:14:56 -05:00
Dan Ribbens
f0dc521718 fix: paginate find chunks 2025-02-03 15:50:21 -05:00
Dan Ribbens
1ce8eb5af8 test: recursion of data 2025-01-29 15:37:37 -05:00
Dan Ribbens
033cd8c7d9 feat: recursion of data 2025-01-29 15:08:57 -05:00
Dan Ribbens
ebc8ea62af feat: flattened array subfield data 2025-01-28 17:01:09 -05:00
Dan Ribbens
25be18a023 feat: flatten csv array data 2025-01-28 16:44:14 -05:00
Dan Ribbens
bf9b9c7b3b test(plugin-import-export): e2e setup 2025-01-24 16:26:13 -05:00
Dan Ribbens
8436b0ba68 feat(plugin-import-export): collection to csv 2025-01-24 15:52:13 -05:00
Dan Ribbens
11b2c3ebb6 feat(plugin-import-export): collection to csv 2025-01-24 15:18:55 -05:00
Dan Ribbens
57800b26da chore(plugin-import-export): test .gitignore 2025-01-24 15:16:16 -05:00
Dan Ribbens
fa63ead3ab merge main 2025-01-23 15:22:58 -05:00
Dan Ribbens
c5b4333fbd Merge branch 'main' into feat/export-ui 2025-01-15 14:54:08 -05:00
Dan Ribbens
bfbc5f4d7b Merge branch 'main' into feat/export-ui 2025-01-04 14:41:13 -05:00
Dan Ribbens
95bac9b5dd feat: WIP csv exports 2024-12-27 16:51:02 -05:00
Dan Ribbens
9db0603229 merge main 2024-12-27 12:13:20 -05:00
Dan Ribbens
3e76465f77 chore(plugin-import-export): e2e ci 2024-12-13 11:42:01 -05:00
Dan Ribbens
b75ac5692f test(plugin-import-export): scaffolding plugin test 2024-12-13 11:31:55 -05:00
Dan Ribbens
7f36c138cd chore(plugin-import-export): scaffolding plugin 2024-12-13 11:28:36 -05:00
Jessica Chowdhury
299f1459e0 feat: adds export button and drawer to list view 2024-12-12 17:02:33 +00:00
273 changed files with 4705 additions and 2995 deletions

4
.github/CODEOWNERS vendored
View File

@@ -8,14 +8,14 @@
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Templates ###
/templates/_data/ @denolfe @jmikrut @DanRibbens
/templates/_template/ @denolfe @jmikrut @DanRibbens
### Build Files ###
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
### Root ###

View File

@@ -311,6 +311,7 @@ jobs:
- i18n
- plugin-cloud-storage
- plugin-form-builder
- plugin-import-export
- plugin-nested-docs
- plugin-seo
- versions

View File

@@ -654,26 +654,6 @@ const ExampleCollection = {
]}
/>
## useDocumentForm
The `useDocumentForm` hook works the same way as the [useForm](#useform) hook, but it always gives you access to the top-level `Form` of a document. This is useful if you need to access the document's `Form` context from within a child `Form`.
An example where this could happen would be custom components within lexical blocks, as lexical blocks initialize their own child `Form`.
```tsx
'use client'
import { useDocumentForm } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
const { fields: parentDocumentFields } = useDocumentForm()
return (
<p>The document's Form has ${Object.keys(parentDocumentFields).length} fields</p>
)
}
```
## useCollapsible
The `useCollapsible` hook allows you to control parent collapsibles:

View File

@@ -158,7 +158,6 @@ The following options are available:
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
| **`afterList`** | An array of components to inject _after_ the built-in List View |
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
| **`listControlsMenu`** | An array of components to render as buttons within a menu next to the List Controls (after the Columns and Filters options) |
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.21.0",
"version": "3.20.0",
"private": true,
"type": "module",
"scripts": {
@@ -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\"",

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.21.0",
"version": "3.20.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -54,7 +54,6 @@ const generateEnvContent = (
.filter((line) => line.includes('=') && !line.startsWith('#'))
.forEach((line) => {
const [key, value] = line.split('=')
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
envVars[key] = value
})

View File

@@ -224,12 +224,12 @@ function insertBeforeAndAfter(content: string, loc: Loc): string {
}
// insert ) after end
lines[end.line - 1] = insert(lines[end.line - 1]!, end.column, ')')
lines[end.line - 1] = insert(lines[end.line - 1], end.column, ')')
// insert withPayload before start
if (start.line === end.line) {
lines[end.line - 1] = insert(lines[end.line - 1]!, start.column, 'withPayload(')
lines[end.line - 1] = insert(lines[end.line - 1], start.column, 'withPayload(')
} else {
lines[start.line - 1] = insert(lines[start.line - 1]!, start.column, 'withPayload(')
lines[start.line - 1] = insert(lines[start.line - 1], start.column, 'withPayload(')
}
return lines.join('\n')

View File

@@ -1,3 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"noUncheckedIndexedAccess": false,
},
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.21.0",
"version": "3.20.0",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.21.0",
"version": "3.20.0",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.21.0",
"version": "3.20.0",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.21.0",
"version": "3.20.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.21.0",
"version": "3.20.0",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -16,9 +16,7 @@ export async function createGlobal<T extends Record<string, unknown>>(
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
data.createdAt = new Date().toISOString()
const result = await upsertRow<{ globalType: string } & T>({
const result = await upsertRow<T>({
adapter: this,
data,
db,
@@ -28,7 +26,5 @@ export async function createGlobal<T extends Record<string, unknown>>(
tableName,
})
result.globalType = slug
return result
}

View File

@@ -97,7 +97,6 @@ export const transformArray = ({
data: arrayRow,
fieldPrefix: '',
fields: field.flattenedFields,
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
parentTableName: arrayTableName,

View File

@@ -101,7 +101,6 @@ export const transformBlocks = ({
data: blockRow,
fieldPrefix: '',
fields: matchedBlock.flattenedFields,
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
parentTableName: blockTableName,

View File

@@ -42,10 +42,6 @@ type Args = {
fieldPrefix: string
fields: FlattenedField[]
forcedLocale?: string
/**
* Tracks whether the current traversion context is from array or block.
*/
insideArrayOrBlock?: boolean
locales: {
[locale: string]: Record<string, unknown>
}
@@ -81,7 +77,6 @@ export const traverseFields = ({
fieldPrefix,
fields,
forcedLocale,
insideArrayOrBlock = false,
locales,
numbers,
parentTableName,
@@ -235,7 +230,6 @@ export const traverseFields = ({
fieldPrefix: `${fieldName}_`,
fields: field.flattenedFields,
forcedLocale: localeKey,
insideArrayOrBlock,
locales,
numbers,
parentTableName,
@@ -264,7 +258,6 @@ export const traverseFields = ({
existingLocales,
fieldPrefix: `${fieldName}_`,
fields: field.flattenedFields,
insideArrayOrBlock,
locales,
numbers,
parentTableName,
@@ -427,7 +420,7 @@ export const traverseFields = ({
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
const newRows = transformSelects({
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
id: data._uuid || data.id,
data: localeData,
locale: localeKey,
})
@@ -438,7 +431,7 @@ export const traverseFields = ({
}
} else if (Array.isArray(data[field.name])) {
const newRows = transformSelects({
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
id: data._uuid || data.id,
data: data[field.name],
locale: withinArrayOrBlockLocale,
})
@@ -479,9 +472,8 @@ export const traverseFields = ({
}
valuesToTransform.forEach(({ localeKey, ref, value }) => {
let formattedValue = value
if (typeof value !== 'undefined') {
let formattedValue = value
if (value && field.type === 'point' && adapter.name !== 'sqlite') {
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
}
@@ -491,16 +483,12 @@ export const traverseFields = ({
formattedValue = new Date(value).toISOString()
} else if (value instanceof Date) {
formattedValue = value.toISOString()
} else if (fieldName === 'updatedAt') {
// let the db handle this
formattedValue = new Date().toISOString()
}
}
}
if (field.type === 'date' && fieldName === 'updatedAt') {
// let the db handle this
formattedValue = new Date().toISOString()
}
if (typeof formattedValue !== 'undefined') {
if (localeKey) {
ref[localeKey][fieldName] = formattedValue
} else {

View File

@@ -17,7 +17,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
const existingGlobal = await db.query[tableName].findFirst({})
const result = await upsertRow<{ globalType: string } & T>({
const result = await upsertRow<T>({
...(existingGlobal ? { id: existingGlobal.id, operation: 'update' } : { operation: 'create' }),
adapter: this,
data,
@@ -28,7 +28,5 @@ export async function updateGlobal<T extends Record<string, unknown>>(
tableName,
})
result.globalType = slug
return result
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.21.0",
"version": "3.20.0",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.21.0",
"version": "3.20.0",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -37,7 +37,7 @@
"eslint-plugin-jest-dom": "5.4.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.9.1",
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-regexp": "2.6.0",
"globals": "15.12.0",
"typescript": "5.7.3",

View File

@@ -36,7 +36,7 @@
"eslint-plugin-jest-dom": "5.4.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.9.1",
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-regexp": "2.6.0",
"globals": "15.12.0",
"typescript": "5.7.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.21.0",
"version": "3.20.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.21.0",
"version": "3.20.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.21.0",
"version": "3.20.0",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.21.0",
"version": "3.20.0",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.21.0",
"version": "3.20.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -91,20 +91,6 @@ export const RootLayout = async ({
importMap,
})
if (
clientConfig.localization &&
config.localization &&
typeof config.localization.filterAvailableLocales === 'function'
) {
clientConfig.localization.locales = (
await config.localization.filterAvailableLocales({
locales: config.localization.locales,
req,
})
).map(({ toString, ...rest }) => rest)
clientConfig.localization.localeCodes = config.localization.locales.map(({ code }) => code)
}
const locale = await getRequestLocale({
req,
})

View File

@@ -91,7 +91,6 @@ export const ForgotPasswordForm: React.FC = () => {
text(value, {
name: 'username',
type: 'text',
blockData: {},
data: {},
event: 'onChange',
preferences: { fields: {} },
@@ -121,7 +120,6 @@ export const ForgotPasswordForm: React.FC = () => {
email(value, {
name: 'email',
type: 'email',
blockData: {},
data: {},
event: 'onChange',
preferences: { fields: {} },

View File

@@ -33,10 +33,10 @@ export const renderListViewSlots = ({
})
}
if (collectionConfig.admin.components?.listControlsMenu) {
result.ListControlsMenu = RenderServerComponent({
if (collectionConfig.admin.components?.afterListControls) {
result.AfterListControls = RenderServerComponent({
clientProps,
Component: collectionConfig.admin.components.listControlsMenu,
Component: collectionConfig.admin.components.afterListControls,
importMap: payload.importMap,
serverProps,
})

View File

@@ -113,7 +113,7 @@ export const buildVersionFields = ({
versionField.fieldByLocale = {}
for (const locale of selectedLocales) {
const localizedVersionField = buildVersionField({
versionField.fieldByLocale[locale] = buildVersionField({
clientField: clientField as ClientField,
clientSchemaMap,
comparisonValue: comparisonValue?.[locale],
@@ -133,12 +133,12 @@ export const buildVersionFields = ({
selectedLocales,
versionValue: versionValue?.[locale],
})
if (localizedVersionField) {
versionField.fieldByLocale[locale] = localizedVersionField
if (!versionField.fieldByLocale[locale]) {
continue
}
}
} else {
const baseVersionField = buildVersionField({
versionField.field = buildVersionField({
clientField: clientField as ClientField,
clientSchemaMap,
comparisonValue,
@@ -158,8 +158,8 @@ export const buildVersionFields = ({
versionValue,
})
if (baseVersionField) {
versionField.field = baseVersionField
if (!versionField.field) {
continue
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.21.0",
"version": "3.20.0",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.21.0",
"version": "3.20.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -68,16 +68,9 @@ export type BuildFormStateArgs = {
data?: Data
docPermissions: SanitizedDocumentPermissions | undefined
docPreferences: DocumentPreferences
/**
* In case `formState` is not the top-level, document form state, this can be passed to
* provide the top-level form state.
*/
documentFormState?: FormState
fallbackLocale?: false | TypedLocale
formState?: FormState
id?: number | string
initialBlockData?: Data
initialBlockFormState?: FormState
/*
If not i18n was passed, the language can be passed to init i18n
*/

View File

@@ -34,7 +34,6 @@ export const generatePasswordSaltHash = async ({
const validationResult = password(passwordToSet, {
name: 'password',
type: 'text',
blockData: {},
data: {},
event: 'submit',
preferences: { fields: {} },

View File

@@ -30,7 +30,7 @@ export function iterateCollections({
})
addToImportMap(collection.admin?.components?.afterList)
addToImportMap(collection.admin?.components?.listControlsMenu)
addToImportMap(collection.admin?.components?.afterListControls)
addToImportMap(collection.admin?.components?.afterListTable)
addToImportMap(collection.admin?.components?.beforeList)
addToImportMap(collection.admin?.components?.beforeListTable)

View File

@@ -276,6 +276,7 @@ export type CollectionAdminOptions = {
*/
components?: {
afterList?: CustomComponent[]
afterListControls?: CustomComponent[]
afterListTable?: CustomComponent[]
beforeList?: CustomComponent[]
beforeListTable?: CustomComponent[]
@@ -310,7 +311,6 @@ export type CollectionAdminOptions = {
*/
Upload?: CustomUpload
}
listControlsMenu?: CustomComponent[]
views?: {
/**
* Set to a React component to replace the entire Edit View, including all nested routes.

View File

@@ -470,14 +470,6 @@ export type BaseLocalizationConfig = {
* @default true
*/
fallback?: boolean
/**
* Define a function to filter the locales made available in Payload admin UI
* based on user.
*/
filterAvailableLocales?: (args: {
locales: Locale[]
req: PayloadRequest
}) => Locale[] | Promise<Locale[]>
}
export type LocalizationConfigWithNoLabels = Prettify<

View File

@@ -133,13 +133,7 @@ import type {
TextareaFieldValidation,
} from '../../index.js'
import type { DocumentPreferences } from '../../preferences/types.js'
import type {
DefaultValue,
JsonObject,
Operation,
PayloadRequest,
Where,
} from '../../types/index.js'
import type { DefaultValue, Operation, PayloadRequest, Where } from '../../types/index.js'
import type {
NumberFieldManyValidation,
NumberFieldSingleValidation,
@@ -154,10 +148,6 @@ import type {
} from '../validations.js'
export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSiblingData = any> = {
/**
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
blockData: JsonObject | undefined
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
collection: null | SanitizedCollectionConfig
context: RequestContext
@@ -222,11 +212,7 @@ export type FieldHook<TData extends TypeWithID = any, TValue = any, TSiblingData
export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (args: {
/**
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
blockData?: JsonObject | undefined
/**
* The incoming, top-level document data used to `create` or `update` the document with.
* The incoming data used to `create` or `update` the document with. `data` is undefined during the `read` operation.
*/
data?: Partial<TData>
/**
@@ -245,33 +231,13 @@ export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (a
siblingData?: Partial<TSiblingData>
}) => boolean | Promise<boolean>
//TODO: In 4.0, we should replace the three parameters of the condition function with a single, named parameter object
export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
/**
* The top-level document data
*/
data: Partial<TData>,
/**
* Immediately adjacent data to this field. For example, if this is a `group` field, then `siblingData` will be the other fields within the group.
*/
siblingData: Partial<TSiblingData>,
{
blockData,
user,
}: {
/**
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
blockData: Partial<TData>
user: PayloadRequest['user']
},
{ user }: { user: PayloadRequest['user'] },
) => boolean
export type FilterOptionsProps<TData = any> = {
/**
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
blockData: TData
/**
* An object containing the full collection or global document currently being edited.
*/
@@ -382,11 +348,6 @@ export type LabelsClient = {
}
export type BaseValidateOptions<TData, TSiblingData, TValue> = {
/**
/**
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
blockData: Partial<TData>
collectionSlug?: string
data: Partial<TData>
event?: 'onChange' | 'submit'

View File

@@ -11,10 +11,6 @@ import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
import { traverseFields } from './traverseFields.js'
type Args = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
data: JsonObject
@@ -37,7 +33,6 @@ type Args = {
// - Execute field hooks
export const promise = async ({
blockData,
collection,
context,
data,
@@ -74,7 +69,6 @@ export const promise = async ({
await priorHook
const hookedValue = await currentHook({
blockData,
collection,
context,
data,
@@ -110,7 +104,6 @@ export const promise = async ({
rows.forEach((row, rowIndex) => {
promises.push(
traverseFields({
blockData,
collection,
context,
data,
@@ -149,7 +142,6 @@ export const promise = async ({
if (block) {
promises.push(
traverseFields({
blockData: siblingData?.[field.name]?.[rowIndex],
collection,
context,
data,
@@ -179,7 +171,6 @@ export const promise = async ({
case 'collapsible':
case 'row': {
await traverseFields({
blockData,
collection,
context,
data,
@@ -202,7 +193,6 @@ export const promise = async ({
case 'group': {
await traverseFields({
blockData,
collection,
context,
data,
@@ -279,7 +269,6 @@ export const promise = async ({
}
await traverseFields({
blockData,
collection,
context,
data,
@@ -302,7 +291,6 @@ export const promise = async ({
case 'tabs': {
await traverseFields({
blockData,
collection,
context,
data,

View File

@@ -7,10 +7,6 @@ import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
type Args = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
data: JsonObject
@@ -29,7 +25,6 @@ type Args = {
}
export const traverseFields = async ({
blockData,
collection,
context,
data,
@@ -51,7 +46,6 @@ export const traverseFields = async ({
fields.forEach((field, fieldIndex) => {
promises.push(
promise({
blockData,
collection,
context,
data,

View File

@@ -19,10 +19,6 @@ import { relationshipPopulationPromise } from './relationshipPopulationPromise.j
import { traverseFields } from './traverseFields.js'
type Args = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
currentDepth: number
@@ -64,7 +60,6 @@ type Args = {
// - Populate relationships
export const promise = async ({
blockData,
collection,
context,
currentDepth,
@@ -241,7 +236,6 @@ export const promise = async ({
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
(async () => {
const hookedValue = await currentHook({
blockData,
collection,
context,
currentDepth,
@@ -272,7 +266,6 @@ export const promise = async ({
await Promise.all(hookPromises)
} else {
const hookedValue = await currentHook({
blockData,
collection,
context,
currentDepth,
@@ -308,7 +301,6 @@ export const promise = async ({
? true
: await field.access.read({
id: doc.id as number | string,
blockData,
data: doc,
doc,
req,
@@ -372,7 +364,6 @@ export const promise = async ({
if (Array.isArray(rows)) {
rows.forEach((row, rowIndex) => {
traverseFields({
blockData,
collection,
context,
currentDepth,
@@ -406,7 +397,6 @@ export const promise = async ({
if (Array.isArray(localeRows)) {
localeRows.forEach((row, rowIndex) => {
traverseFields({
blockData,
collection,
context,
currentDepth,
@@ -486,7 +476,6 @@ export const promise = async ({
if (block) {
traverseFields({
blockData: row,
collection,
context,
currentDepth,
@@ -526,7 +515,6 @@ export const promise = async ({
if (block) {
traverseFields({
blockData: row,
collection,
context,
currentDepth,
@@ -566,7 +554,6 @@ export const promise = async ({
case 'collapsible':
case 'row': {
traverseFields({
blockData,
collection,
context,
currentDepth,
@@ -608,7 +595,6 @@ export const promise = async ({
const groupSelect = select?.[field.name]
traverseFields({
blockData,
collection,
context,
currentDepth,
@@ -761,7 +747,6 @@ export const promise = async ({
}
traverseFields({
blockData,
collection,
context,
currentDepth,
@@ -795,7 +780,6 @@ export const promise = async ({
case 'tabs': {
traverseFields({
blockData,
collection,
context,
currentDepth,

View File

@@ -13,10 +13,6 @@ import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
type Args = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
currentDepth: number
@@ -49,7 +45,6 @@ type Args = {
}
export const traverseFields = ({
blockData,
collection,
context,
currentDepth,
@@ -80,7 +75,6 @@ export const traverseFields = ({
fields.forEach((field, fieldIndex) => {
fieldPromises.push(
promise({
blockData,
collection,
context,
currentDepth,

View File

@@ -4,7 +4,7 @@ import type { ValidationFieldError } from '../../../errors/index.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { RequestContext } from '../../../index.js'
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
import type { Field, TabAsField, Validate } from '../../config/types.js'
import type { Field, TabAsField } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
@@ -16,10 +16,6 @@ import { getExistingRowDoc } from './getExistingRowDoc.js'
import { traverseFields } from './traverseFields.js'
type Args = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
data: JsonObject
@@ -52,7 +48,6 @@ type Args = {
export const promise = async ({
id,
blockData,
collection,
context,
data,
@@ -82,7 +77,7 @@ export const promise = async ({
})
const passesCondition = field.admin?.condition
? Boolean(field.admin.condition(data, siblingData, { blockData, user: req.user }))
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
: true
let skipValidationFromHere = skipValidation || !passesCondition
const { localization } = req.payload.config
@@ -107,7 +102,6 @@ export const promise = async ({
await priorHook
const hookedValue = await currentHook({
blockData,
collection,
context,
data,
@@ -145,27 +139,22 @@ export const promise = async ({
}
}
const validateFn: Validate<object, object, object, object> = field.validate as Validate<
object,
object,
object,
object
>
const validationResult = await validateFn(valueToValidate as never, {
...field,
id,
blockData,
collectionSlug: collection?.slug,
data: deepMergeWithSourceArrays(doc, data),
event: 'submit',
// @ts-expect-error
jsonError,
operation,
preferences: { fields: {} },
previousValue: siblingDoc[field.name],
req,
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
})
const validationResult = await field.validate(
valueToValidate as never,
{
...field,
id,
collectionSlug: collection?.slug,
data: deepMergeWithSourceArrays(doc, data),
event: 'submit',
jsonError,
operation,
preferences: { fields: {} },
previousValue: siblingDoc[field.name],
req,
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
} as any,
)
if (typeof validationResult === 'string') {
const label = getTranslatedLabel(field?.label || field?.name, req.i18n)
@@ -228,7 +217,6 @@ export const promise = async ({
promises.push(
traverseFields({
id,
blockData,
collection,
context,
data,
@@ -280,7 +268,6 @@ export const promise = async ({
promises.push(
traverseFields({
id,
blockData: row,
collection,
context,
data,
@@ -314,7 +301,6 @@ export const promise = async ({
case 'row': {
await traverseFields({
id,
blockData,
collection,
context,
data,
@@ -353,7 +339,6 @@ export const promise = async ({
await traverseFields({
id,
blockData,
collection,
context,
data,
@@ -470,7 +455,6 @@ export const promise = async ({
await traverseFields({
id,
blockData,
collection,
context,
data,
@@ -497,7 +481,6 @@ export const promise = async ({
case 'tabs': {
await traverseFields({
id,
blockData,
collection,
context,
data,

View File

@@ -8,10 +8,6 @@ import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
type Args = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
data: JsonObject
@@ -55,7 +51,6 @@ type Args = {
*/
export const traverseFields = async ({
id,
blockData,
collection,
context,
data,
@@ -81,7 +76,6 @@ export const traverseFields = async ({
promises.push(
promise({
id,
blockData,
collection,
context,
data,

View File

@@ -9,10 +9,6 @@ import { runBeforeDuplicateHooks } from './runHook.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
doc: T
@@ -29,7 +25,6 @@ type Args<T> = {
export const promise = async <T>({
id,
blockData,
collection,
context,
doc,
@@ -68,7 +63,6 @@ export const promise = async <T>({
const localizedValues = await localizedValuesPromise
const beforeDuplicateArgs: FieldHookArgs = {
blockData,
collection,
context,
data: doc,
@@ -102,7 +96,6 @@ export const promise = async <T>({
siblingDoc[field.name] = localeData
} else {
const beforeDuplicateArgs: FieldHookArgs = {
blockData,
collection,
context,
data: doc,
@@ -150,7 +143,6 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
blockData,
collection,
context,
doc,
@@ -185,7 +177,6 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
blockData: row,
collection,
context,
doc,
@@ -208,7 +199,6 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
blockData,
collection,
context,
doc,
@@ -244,7 +234,6 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
blockData,
collection,
context,
doc,
@@ -281,7 +270,6 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
blockData: row,
collection,
context,
doc,
@@ -312,7 +300,6 @@ export const promise = async <T>({
await traverseFields({
id,
blockData,
collection,
context,
doc,
@@ -337,7 +324,6 @@ export const promise = async <T>({
await traverseFields({
id,
blockData,
collection,
context,
doc,
@@ -361,7 +347,6 @@ export const promise = async <T>({
case 'row': {
await traverseFields({
id,
blockData,
collection,
context,
doc,
@@ -382,7 +367,6 @@ export const promise = async <T>({
case 'tab': {
await traverseFields({
id,
blockData,
collection,
context,
doc,
@@ -402,7 +386,6 @@ export const promise = async <T>({
case 'tabs': {
await traverseFields({
id,
blockData,
collection,
context,
doc,

View File

@@ -6,10 +6,6 @@ import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
type Args<T> = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
doc: T
@@ -25,7 +21,6 @@ type Args<T> = {
export const traverseFields = async <T>({
id,
blockData,
collection,
context,
doc,
@@ -43,7 +38,6 @@ export const traverseFields = async <T>({
promises.push(
promise({
id,
blockData,
collection,
context,
doc,

View File

@@ -14,10 +14,6 @@ import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
data: T
@@ -51,7 +47,6 @@ type Args<T> = {
export const promise = async <T>({
id,
blockData,
collection,
context,
data,
@@ -275,7 +270,6 @@ export const promise = async <T>({
await priorHook
const hookedValue = await currentHook({
blockData,
collection,
context,
data,
@@ -304,7 +298,7 @@ export const promise = async <T>({
if (field.access && field.access[operation]) {
const result = overrideAccess
? true
: await field.access[operation]({ id, blockData, data, doc, req, siblingData })
: await field.access[operation]({ id, data, doc, req, siblingData })
if (!result) {
delete siblingData[field.name]
@@ -341,7 +335,6 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
blockData,
collection,
context,
data,
@@ -382,7 +375,6 @@ export const promise = async <T>({
promises.push(
traverseFields({
id,
blockData: row,
collection,
context,
data,
@@ -412,7 +404,6 @@ export const promise = async <T>({
case 'row': {
await traverseFields({
id,
blockData,
collection,
context,
data,
@@ -446,7 +437,6 @@ export const promise = async <T>({
await traverseFields({
id,
blockData,
collection,
context,
data,
@@ -532,7 +522,6 @@ export const promise = async <T>({
await traverseFields({
id,
blockData,
collection,
context,
data,
@@ -555,7 +544,6 @@ export const promise = async <T>({
case 'tabs': {
await traverseFields({
id,
blockData,
collection,
context,
data,

View File

@@ -7,10 +7,6 @@ import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
type Args<T> = {
/**
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
*/
blockData?: JsonObject
collection: null | SanitizedCollectionConfig
context: RequestContext
data: T
@@ -36,7 +32,6 @@ type Args<T> = {
export const traverseFields = async <T>({
id,
blockData,
collection,
context,
data,
@@ -58,7 +53,6 @@ export const traverseFields = async <T>({
promises.push(
promise({
id,
blockData,
collection,
context,
data,

View File

@@ -510,7 +510,7 @@ const validateFilterOptions: Validate<
RelationshipField | UploadField
> = async (
value,
{ id, blockData, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
{ id, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
) => {
if (typeof filterOptions !== 'undefined' && value) {
const options: {
@@ -527,7 +527,6 @@ const validateFilterOptions: Validate<
typeof filterOptions === 'function'
? await filterOptions({
id,
blockData,
data,
relationTo: collection,
req,

View File

@@ -244,11 +244,6 @@ export const updateOperation = async <
// /////////////////////////////////////
if (!shouldSaveDraft) {
// Ensure global has createdAt
if (!result.createdAt) {
result.createdAt = new Date().toISOString()
}
if (globalExists) {
result = await payload.db.updateGlobal({
slug,

View File

@@ -217,9 +217,7 @@ function entityOrFieldToJsDocs({
description = entity?.admin?.description?.[i18n.language]
}
} else if (typeof entity?.admin?.description === 'function' && i18n) {
// do not evaluate description functions for generating JSDocs. The output of
// those can differ depending on where and when they are called, creating
// inconsistencies in the generated JSDocs.
description = entity?.admin?.description(i18n)
}
}
return description

View File

@@ -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 = () => {

View File

@@ -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',

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.21.0",
"version": "3.20.0",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.21.0",
"version": "3.20.0",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -0,0 +1,7 @@
node_modules
.env
dist
demo/uploads
build
.DS_Store
package-lock.json

View File

@@ -0,0 +1,12 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
**/docs/**
tsconfig.json

View 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"
}
}

View 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.

View 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

View 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.

View 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"
}

View 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;
}
}
}

View 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>
)

View File

@@ -0,0 +1,5 @@
.export-button {
cursor: pointer;
width: 100%;
text-align: left;
}

View File

@@ -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>
)
}

View File

@@ -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'],
},
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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>
)
}

View 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,
},
})
}
}
}

View File

@@ -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',
},
],
}

View 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
}

View 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}`
}

View 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
}

View 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[]
}

View 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
// }
// }

View 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,
// },
]

View File

@@ -0,0 +1 @@
export { ExportButton } from '../components/ExportButton/index.js'

View File

@@ -0,0 +1 @@
export type { ImportExportPluginConfig } from '../types.js'

View 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
}

View 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
}

View 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',
},
}

View 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>

View File

@@ -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"]
}

View 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
}

View 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"}]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-multi-tenant",
"version": "3.21.0",
"version": "3.20.0",
"description": "Multi Tenant plugin for Payload",
"keywords": [
"payload",

View File

@@ -24,21 +24,15 @@ export const TenantField = (args: Props) => {
const hasSetValueRef = React.useRef(false)
React.useEffect(() => {
if (!hasSetValueRef.current) {
if (!hasSetValueRef.current && value) {
// set value on load
if (value && value !== selectedTenantID) {
setTenant({ id: value, refresh: unique })
} else {
// in the document view, the tenant field should always have a value
const defaultValue =
!selectedTenantID || selectedTenantID === SELECT_ALL
? options[0]?.value
: selectedTenantID
setTenant({ id: defaultValue, refresh: unique })
}
setTenant({ id: value, refresh: unique })
hasSetValueRef.current = true
} else if ((!value || value !== selectedTenantID) && selectedTenantID !== SELECT_ALL) {
// Update the field on the document value when the tenant is changed
} else if (selectedTenantID && selectedTenantID === SELECT_ALL && options?.[0]?.value) {
// in the document view, the tenant field should always have a value
setTenant({ id: options[0].value, refresh: unique })
} else if ((!value || value !== selectedTenantID) && selectedTenantID) {
// Update the field value when the tenant is changed
setValue(selectedTenantID)
}
}, [value, selectedTenantID, setTenant, setValue, options, unique])

View File

@@ -1,6 +0,0 @@
export const defaults = {
tenantCollectionSlug: 'tenants',
tenantFieldName: 'tenant',
tenantsArrayFieldName: 'tenants',
tenantsArrayTenantFieldName: 'tenant',
}

View File

@@ -1,7 +1,6 @@
import { type RelationshipField } from 'payload'
import { APIError } from 'payload'
import { defaults } from '../../defaults.js'
import { getCollectionIDType } from '../../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from '../../utilities/getTenantFromCookie.js'
@@ -13,10 +12,10 @@ type Args = {
unique: boolean
}
export const tenantField = ({
name = defaults.tenantFieldName,
name,
access = undefined,
debug,
tenantsCollectionSlug = defaults.tenantCollectionSlug,
tenantsCollectionSlug,
unique,
}: Args): RelationshipField => ({
name,

View File

@@ -1,37 +1,27 @@
import type { ArrayField, RelationshipField } from 'payload'
import { defaults } from '../../defaults.js'
type Args = {
export const tenantsArrayField = (args: {
arrayFieldAccess?: ArrayField['access']
rowFields?: ArrayField['fields']
tenantFieldAccess?: RelationshipField['access']
tenantsArrayFieldName: ArrayField['name']
tenantsArrayTenantFieldName: RelationshipField['name']
tenantsCollectionSlug: string
}
export const tenantsArrayField = ({
arrayFieldAccess,
rowFields,
tenantFieldAccess,
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
tenantsArrayTenantFieldName = defaults.tenantsArrayFieldName,
tenantsCollectionSlug = defaults.tenantCollectionSlug,
}: Args): ArrayField => ({
name: tenantsArrayFieldName,
}): ArrayField => ({
name: args.tenantsArrayFieldName,
type: 'array',
access: arrayFieldAccess,
access: args?.arrayFieldAccess,
fields: [
{
name: tenantsArrayTenantFieldName,
name: args.tenantsArrayTenantFieldName,
type: 'relationship',
access: tenantFieldAccess,
access: args.tenantFieldAccess,
index: true,
relationTo: tenantsCollectionSlug,
relationTo: args.tenantsCollectionSlug,
required: true,
saveToJWT: true,
},
...(rowFields || []),
...(args?.rowFields || []),
],
saveToJWT: true,
})

View File

@@ -2,7 +2,6 @@ import type { CollectionConfig, Config } from 'payload'
import type { MultiTenantPluginConfig } from './types.js'
import { defaults } from './defaults.js'
import { tenantField } from './fields/tenantField/index.js'
import { tenantsArrayField } from './fields/tenantsArrayField/index.js'
import { addTenantCleanup } from './hooks/afterTenantDelete.js'
@@ -10,6 +9,13 @@ import { addCollectionAccess } from './utilities/addCollectionAccess.js'
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
import { withTenantListFilter } from './utilities/withTenantListFilter.js'
const defaults = {
tenantCollectionSlug: 'tenants',
tenantFieldName: 'tenant',
tenantsArrayFieldName: 'tenants',
tenantsArrayTenantFieldName: 'tenant',
}
export const multiTenantPlugin =
<ConfigType>(pluginConfig: MultiTenantPluginConfig<ConfigType>) =>
(incomingConfig: Config): Config => {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.21.0",
"version": "3.20.0",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.21.0",
"version": "3.20.0",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.21.0",
"version": "3.20.0",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -42,25 +42,10 @@ export type SearchPluginConfig = {
defaultPriorities?: {
[collection: string]: ((doc: any) => number | Promise<number>) | number
}
/**
* Controls whether drafts are deleted from the search index
*
* @default true
*/
deleteDrafts?: boolean
localize?: boolean
/**
* We use batching when re-indexing large collections. You can control the amount of items per batch, lower numbers should help with memory.
*
* @default 50
*/
reindexBatchSize?: number
searchOverrides?: { fields?: FieldsOverride } & Partial<Omit<CollectionConfig, 'fields'>>
/**
* Controls whether drafts are synced to the search index
*
* @default false
*/
syncDrafts?: boolean
}

View File

@@ -142,42 +142,15 @@ export const syncDocAsSearchIndex = async ({
}
}
if (deleteDrafts && status === 'draft') {
// Check to see if there's a published version of the doc
// We don't want to remove the search doc if there is a published version but a new draft has been created
const {
docs: [docWithPublish],
} = await payload.find({
collection,
draft: false,
locale: syncLocale,
req,
where: {
and: [
{
_status: {
equals: 'published',
},
},
{
id: {
equals: id,
},
},
],
},
})
if (!docWithPublish) {
// do not include draft docs in search results, so delete the record
try {
await payload.delete({
id: searchDocID,
collection: searchSlug,
req,
})
} catch (err: unknown) {
payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` })
}
// do not include draft docs in search results, so delete the record
try {
await payload.delete({
id: searchDocID,
collection: searchSlug,
req,
})
} catch (err: unknown) {
payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` })
}
}
} else if (doSync) {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
"version": "3.21.0",
"version": "3.20.0",
"description": "Sentry plugin for Payload",
"keywords": [
"payload",

Some files were not shown because too many files have changed in this diff Show More