Compare commits
41 Commits
chore/list
...
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 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -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 ###
|
||||
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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\"",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.21.0",
|
||||
"version": "3.20.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ export const transformArray = ({
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: arrayTableName,
|
||||
|
||||
@@ -101,7 +101,6 @@ export const transformBlocks = ({
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
fields: matchedBlock.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: blockTableName,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.21.0",
|
||||
"version": "3.20.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.21.0",
|
||||
"version": "3.20.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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: {} },
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -34,7 +34,6 @@ export const generatePasswordSaltHash = async ({
|
||||
const validationResult = password(passwordToSet, {
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'submit',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
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"}]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export const defaults = {
|
||||
tenantCollectionSlug: 'tenants',
|
||||
tenantFieldName: 'tenant',
|
||||
tenantsArrayFieldName: 'tenants',
|
||||
tenantsArrayTenantFieldName: 'tenant',
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.21.0",
|
||||
"version": "3.20.0",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.21.0",
|
||||
"version": "3.20.0",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user