Compare commits

..

1 Commits

Author SHA1 Message Date
Jacob Fletcher
09d29a6ec9 feat(next): performs serverside redirect for limit query param 2025-02-24 16:46:49 -05:00
80 changed files with 1353 additions and 1604 deletions

37
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,37 @@
# Order matters. The last matching pattern takes precedence.
### Package Exports
**/exports/ @denolfe @jmikrut @DanRibbens
### Packages
/packages/plugin-cloud*/src/ @denolfe @jmikrut @DanRibbens
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
/packages/live-preview*/src/ @jacobsfletch
/packages/plugin-stripe/src/ @jacobsfletch
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
/packages/richtext-*/src/ @AlessioGr
/packages/next/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/ui/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
/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
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
### Root
/package.json @denolfe @jmikrut @DanRibbens
/tools/ @denolfe @jmikrut @DanRibbens
/.husky/ @denolfe @jmikrut @DanRibbens
/.vscode/ @denolfe @jmikrut @DanRibbens @AlessioGr
/.github/ @denolfe @jmikrut @DanRibbens

View File

@@ -1,6 +1,5 @@
import type {
AdminViewServerProps,
ColumnPreference,
ListPreferences,
ListQuery,
ListViewClientProps,
@@ -12,7 +11,7 @@ import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloa
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import { notFound, redirect } from 'next/navigation.js'
import { isNumber } from 'payload/shared'
import React, { Fragment } from 'react'
@@ -72,23 +71,13 @@ export const renderListView = async (
}
const query = queryFromArgs || queryFromReq
let columns: ColumnPreference[]
if (query.columns) {
try {
columns = JSON.parse(query?.columns as string) as ColumnPreference[]
} catch (error) {
console.error('Error parsing columns from URL:', error) // eslint-disable-line no-console
}
}
const limitFromQuery = isNumber(query?.limit) ? Number(query.limit) : undefined
const listPreferences = await upsertPreferences<ListPreferences>({
key: `${collectionSlug}-list`,
req,
value: {
columns,
limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
limit: limitFromQuery,
sort: query?.sort as string,
},
})
@@ -153,7 +142,6 @@ export const renderListView = async (
clientCollectionConfig,
collectionConfig,
columnPreferences: listPreferences?.columns,
columns,
customCellProps,
docs: data.docs,
drawerSlug,
@@ -216,11 +204,9 @@ export const renderListView = async (
<Fragment>
<HydrateAuthProvider permissions={permissions} />
<ListQueryProvider
columns={columnState.map(({ accessor, active }) => ({ [accessor]: active }))}
data={data}
defaultLimit={limit}
defaultSort={sort}
listPreferences={listPreferences}
modifySearchParams={!isInDrawer}
>
{RenderServerComponent({
@@ -253,6 +239,19 @@ export const renderListView = async (
}
export const ListView: React.FC<RenderListViewArgs> = async (args) => {
const {
initPageResult: { collectionConfig, req },
} = args
if (!req.query?.limit) {
return redirect(
`${req.url}?${new URLSearchParams({
...req.query,
limit: String(collectionConfig.admin.pagination.defaultLimit),
}).toString()}`,
)
}
try {
const { List: RenderedList } = await renderListView({ ...args, enableRowSelections: true })
return RenderedList

View File

@@ -1,13 +1,19 @@
import type * as AWS from '@aws-sdk/client-s3'
import type { CognitoUserSession } from 'amazon-cognito-identity-js'
import type { GetStorageClient } from './refreshSession.js'
import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'
import * as AWS from '@aws-sdk/client-s3'
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers'
import { refreshSession } from './refreshSession.js'
import { authAsCognitoUser } from './authAsCognitoUser.js'
export let storageClient: AWS.S3 | null = null
export let session: CognitoUserSession | null = null
export let identityID: string
export type GetStorageClient = () => Promise<{
identityID: string
storageClient: AWS.S3
}>
let storageClient: AWS.S3 | null = null
let session: CognitoUserSession | null = null
let identityID: string
export const getStorageClient: GetStorageClient = async () => {
if (storageClient && session?.isValid()) {
@@ -17,8 +23,6 @@ export const getStorageClient: GetStorageClient = async () => {
}
}
;({ identityID, session, storageClient } = await refreshSession())
if (!process.env.PAYLOAD_CLOUD_PROJECT_ID) {
throw new Error('PAYLOAD_CLOUD_PROJECT_ID is required')
}
@@ -29,6 +33,34 @@ export const getStorageClient: GetStorageClient = async () => {
throw new Error('PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID is required')
}
session = await authAsCognitoUser(
process.env.PAYLOAD_CLOUD_PROJECT_ID,
process.env.PAYLOAD_CLOUD_COGNITO_PASSWORD,
)
const cognitoIdentity = new CognitoIdentityClient({
credentials: fromCognitoIdentityPool({
clientConfig: {
region: 'us-east-1',
},
identityPoolId: process.env.PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID,
logins: {
[`cognito-idp.us-east-1.amazonaws.com/${process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID}`]:
session.getIdToken().getJwtToken(),
},
}),
})
const credentials = await cognitoIdentity.config.credentials()
// @ts-expect-error - Incorrect AWS types
identityID = credentials.identityId
storageClient = new AWS.S3({
credentials,
region: process.env.PAYLOAD_CLOUD_BUCKET_REGION,
})
return {
identityID,
storageClient,

View File

@@ -1,46 +0,0 @@
import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'
import * as AWS from '@aws-sdk/client-s3'
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers'
import { authAsCognitoUser } from './authAsCognitoUser.js'
export type GetStorageClient = () => Promise<{
identityID: string
storageClient: AWS.S3
}>
export const refreshSession = async () => {
const session = await authAsCognitoUser(
process.env.PAYLOAD_CLOUD_PROJECT_ID || '',
process.env.PAYLOAD_CLOUD_COGNITO_PASSWORD || '',
)
const cognitoIdentity = new CognitoIdentityClient({
credentials: fromCognitoIdentityPool({
clientConfig: {
region: 'us-east-1',
},
identityPoolId: process.env.PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID || '',
logins: {
[`cognito-idp.us-east-1.amazonaws.com/${process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID}`]:
session.getIdToken().getJwtToken(),
},
}),
})
const credentials = await cognitoIdentity.config.credentials()
// @ts-expect-error - Incorrect AWS types
const identityID = credentials.identityId
const storageClient = new AWS.S3({
credentials,
region: process.env.PAYLOAD_CLOUD_BUCKET_REGION,
})
return {
identityID,
session,
storageClient,
}
}

View File

@@ -96,7 +96,6 @@
"dataloader": "2.2.3",
"deepmerge": "4.3.1",
"file-type": "19.3.0",
"fractional-indexing": "3.2.0",
"get-tsconfig": "4.8.1",
"http-status": "2.1.0",
"image-size": "1.2.0",

View File

@@ -1,7 +1,7 @@
import type { ImportMap } from '../../bin/generateImportMap/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { PaginatedDocs } from '../../database/types.js'
import type { CollectionSlug, ColumnPreference } from '../../index.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
export type DefaultServerFunctionArgs = {
@@ -38,7 +38,6 @@ export type ServerFunctionHandler = (
) => Promise<unknown>
export type ListQuery = {
columns?: ColumnPreference[]
limit?: string
page?: string
/*
@@ -51,7 +50,7 @@ export type ListQuery = {
export type BuildTableStateArgs = {
collectionSlug: string | string[]
columns?: ColumnPreference[]
columns?: { accessor: string; active: boolean }[]
docs?: PaginatedDocs['docs']
enableRowSelections?: boolean
parent?: {

View File

@@ -42,14 +42,8 @@ export type ListViewClientProps = {
disableBulkEdit?: boolean
enableRowSelections?: boolean
hasCreatePermission: boolean
/**
* @deprecated
*/
listPreferences?: ListPreferences
newDocumentURL: string
/**
* @deprecated
*/
preferenceKey?: string
renderedFilters?: Map<string, React.ReactNode>
resolvedFilterOptions?: Map<string, ResolvedFilterOptions>

View File

@@ -1,19 +1,20 @@
// @ts-strict-ignore
import type { AuthStrategyFunctionArgs, AuthStrategyResult } from './index.js'
export const executeAuthStrategies = async (
args: AuthStrategyFunctionArgs,
): Promise<AuthStrategyResult> => {
if (!args.payload.authStrategies?.length) {
return { user: null }
}
return args.payload.authStrategies.reduce(
async (accumulatorPromise, strategy) => {
const result: AuthStrategyResult = await accumulatorPromise
if (!result.user) {
// add the configured AuthStrategy `name` to the strategy function args
args.strategyName = strategy.name
for (const strategy of args.payload.authStrategies) {
// add the configured AuthStrategy `name` to the strategy function args
args.strategyName = strategy.name
const result = await strategy.authenticate(args)
if (result.user) {
return strategy.authenticate(args)
}
return result
}
}
return { user: null }
},
Promise.resolve({ user: null }),
)
}

View File

@@ -64,18 +64,18 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'forgotPassword',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'forgotPassword',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
@@ -190,11 +190,10 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
// afterForgotPassword - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterForgotPassword?.length) {
for (const hook of collectionConfig.hooks.afterForgotPassword) {
await hook({ args, collection: args.collection?.config, context: req.context })
}
}
await collectionConfig.hooks.afterForgotPassword.reduce(async (priorHook, hook) => {
await priorHook
await hook({ args, collection: args.collection?.config, context: req.context })
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -51,18 +51,18 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
@@ -227,17 +227,17 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// beforeLogin - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeLogin?.length) {
for (const hook of collectionConfig.hooks.beforeLogin) {
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
user,
})) || user
}
}
await collectionConfig.hooks.beforeLogin.reduce(async (priorHook, hook) => {
await priorHook
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
user,
})) || user
}, Promise.resolve())
const { exp, token } = await jwtSign({
fieldsToSign,
@@ -251,18 +251,18 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// afterLogin - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterLogin?.length) {
for (const hook of collectionConfig.hooks.afterLogin) {
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
token,
user,
})) || user
}
}
await collectionConfig.hooks.afterLogin.reduce(async (priorHook, hook) => {
await priorHook
user =
(await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
token,
user,
})) || user
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -286,17 +286,17 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
user =
(await hook({
collection: args.collection?.config,
context: req.context,
doc: user,
req,
})) || user
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
user =
(await hook({
collection: args.collection?.config,
context: req.context,
doc: user,
req,
})) || user
}, Promise.resolve())
let result: { user: DataFromCollectionSlug<TSlug> } & Result = {
exp,

View File

@@ -25,16 +25,16 @@ export const logoutOperation = async (incomingArgs: Arguments): Promise<boolean>
throw new APIError('Incorrect collection', httpStatus.FORBIDDEN)
}
if (collectionConfig.hooks?.afterLogout?.length) {
for (const hook of collectionConfig.hooks.afterLogout) {
args =
(await hook({
collection: args.collection?.config,
context: req.context,
req,
})) || args
}
}
await collectionConfig.hooks.afterLogout.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
collection: args.collection?.config,
context: req.context,
req,
})) || args
}, Promise.resolve())
return true
}

View File

@@ -86,17 +86,17 @@ export const meOperation = async (args: Arguments): Promise<MeOperationResult> =
// After Me - Collection
// /////////////////////////////////////
if (collection.config.hooks?.afterMe?.length) {
for (const hook of collection.config.hooks.afterMe) {
result =
(await hook({
collection: collection?.config,
context: req.context,
req,
response: result,
})) || result
}
}
await collection.config.hooks.afterMe.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collection?.config,
context: req.context,
req,
response: result,
})) || result
}, Promise.resolve())
return result
}

View File

@@ -35,8 +35,10 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -45,8 +47,9 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
operation: 'refresh',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
// /////////////////////////////////////
// Refresh
@@ -119,18 +122,18 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
// After Refresh - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRefresh?.length) {
for (const hook of collectionConfig.hooks.afterRefresh) {
result =
(await hook({
collection: args.collection?.config,
context: args.req.context,
exp: result.exp,
req: args.req,
token: result.refreshedToken,
})) || result
}
}
await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: args.collection?.config,
context: args.req.context,
exp: result.exp,
req: args.req,
token: result.refreshedToken,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -91,17 +91,17 @@ export const resetPasswordOperation = async (args: Arguments): Promise<Result> =
// beforeValidate - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeValidate?.length) {
for (const hook of collectionConfig.hooks.beforeValidate) {
await hook({
collection: args.collection?.config,
context: req.context,
data: user,
operation: 'update',
req,
})
}
}
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
await hook({
collection: args.collection?.config,
context: req.context,
data: user,
operation: 'update',
req,
})
}, Promise.resolve())
// /////////////////////////////////////
// Update new password

View File

@@ -1,11 +1,7 @@
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing'
// @ts-strict-ignore
import type { LoginWithUsernameOptions } from '../../auth/types.js'
import type { Config, Endpoint, PayloadHandler, SanitizedConfig } from '../../config/types.js'
import type { Field } from '../../fields/config/types.js'
import type { Config, SanitizedConfig } from '../../config/types.js'
import type {
BeforeChangeHook,
CollectionConfig,
SanitizedCollectionConfig,
SanitizedJoin,
@@ -242,134 +238,6 @@ export const sanitizeCollection = async (
validateUseAsTitle(sanitized)
// duplicated in the UI package too. Don't change one without changing the other.
const ORDER_FIELD_NAME = 'payload-order'
// Enable custom order
if (collection.enableCustomOrder) {
// 1. Add field
const orderField: Field = {
name: ORDER_FIELD_NAME,
type: 'text',
admin: {
disableBulkEdit: true,
hidden: true,
},
index: true,
label: 'Order',
}
sanitized.fields.unshift(orderField)
// 2. Add hook
if (!sanitized.hooks) {
sanitized.hooks = {}
}
if (!sanitized.hooks.beforeChange) {
sanitized.hooks.beforeChange = []
}
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, operation, req }) => {
// Only set _order on create, not on update (unless explicitly provided)
if (operation === 'create') {
// Find the last document to place this one after
const lastDoc = await req.payload.find({
collection: sanitized.slug,
depth: 0,
limit: 1,
sort: `-${ORDER_FIELD_NAME}`,
})
const lastOrderValue = lastDoc.docs[0]?.[ORDER_FIELD_NAME] || null
data[ORDER_FIELD_NAME] = generateKeyBetween(lastOrderValue, null)
}
return data
}
sanitized.hooks.beforeChange.push(orderBeforeChangeHook)
// 3. Add endpoint
const moveBetweenHandler: PayloadHandler = async (req) => {
const body = await req.json()
const { betweenIds, docIds } = body as {
betweenIds: [string | undefined, string | undefined] // tuple [beforeId, afterId]
docIds: string[] // array of docIds to be moved between the two reference points
}
if (!Array.isArray(docIds) || docIds.length === 0) {
return new Response(JSON.stringify({ error: 'Invalid or empty docIds array' }), {
headers: { 'Content-Type': 'application/json' },
status: 400,
})
}
if (!Array.isArray(betweenIds) || betweenIds.length !== 2) {
return new Response(
JSON.stringify({ error: 'betweenIds must be a tuple of two elements' }),
{
headers: { 'Content-Type': 'application/json' },
status: 400,
},
)
}
const [beforeId, afterId] = betweenIds
// Fetch the order values of the documents we're inserting between
let beforeOrderValue = null
let afterOrderValue = null
// TODO: maybe the endpoint can receive directly the order values?
if (beforeId) {
const beforeDoc = await req.payload.findByID({
id: beforeId,
collection: sanitized.slug,
})
beforeOrderValue = beforeDoc?.[ORDER_FIELD_NAME] || null
}
if (afterId) {
const afterDoc = await req.payload.findByID({
id: afterId,
collection: sanitized.slug,
})
afterOrderValue = afterDoc?.[ORDER_FIELD_NAME] || null
}
const orderValues = generateNKeysBetween(beforeOrderValue, afterOrderValue, docIds.length)
// Update each document with its new order value
const updatePromises = docIds.map((id, index) => {
return req.payload.update({
id,
collection: sanitized.slug,
data: {
[ORDER_FIELD_NAME]: orderValues[index],
},
})
})
const results = await Promise.all(updatePromises)
return new Response(JSON.stringify({ results, success: true }), {
headers: { 'Content-Type': 'application/json' },
status: 200,
})
}
const moveBetweenEndpoint: Endpoint = {
handler: moveBetweenHandler,
method: 'post',
path: '/reorder',
}
if (!sanitized.endpoints) {
sanitized.endpoints = []
}
sanitized.endpoints.push(moveBetweenEndpoint)
}
const sanitizedConfig = sanitized as SanitizedCollectionConfig
sanitizedConfig.joins = joins

View File

@@ -409,15 +409,6 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
* When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs
*/
disableDuplicate?: boolean
/**
* If true, enables custom ordering for the collection, and documents in the listView can be reordered via drag and drop.
* New documents are inserted at the end of the list according to this parameter.
*
* Under the hood, a field with {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing|fractional indexing} is used to optimize inserts and reorderings.
*
* @default false
*/
enableCustomOrder?: boolean
/**
* Custom rest api endpoints, set false to disable all rest endpoints for this collection.
*/

View File

@@ -44,9 +44,7 @@ const batchAndLoadDocs =
*
**/
const batchByFindArgs = {}
for (const key of keys) {
const batchByFindArgs = keys.reduce((batches, key) => {
const [
transactionID,
collection,
@@ -79,16 +77,27 @@ const batchAndLoadDocs =
const batchKey = JSON.stringify(batchKeyArray)
const idType = payload.collections?.[collection].customIDType || payload.db.defaultIDType
const sanitizedID = idType === 'number' ? parseFloat(id) : id
let sanitizedID: number | string = id
if (idType === 'number') {
sanitizedID = parseFloat(id)
}
if (isValidID(sanitizedID, idType)) {
batchByFindArgs[batchKey] = [...(batchByFindArgs[batchKey] || []), sanitizedID]
return {
...batches,
[batchKey]: [...(batches[batchKey] || []), sanitizedID],
}
}
}
return batches
}, {})
// Run find requests one after another, so as to not hang transactions
for (const [batchKey, ids] of Object.entries(batchByFindArgs)) {
await Object.entries(batchByFindArgs).reduce(async (priorFind, [batchKey, ids]) => {
await priorFind
const [
transactionID,
collection,
@@ -128,7 +137,8 @@ const batchAndLoadDocs =
// For each returned doc, find index in original keys
// Inject doc within docs array if index exists
for (const doc of result.docs) {
result.docs.forEach((doc) => {
const docKey = createDataloaderCacheKey({
collectionSlug: collection,
currentDepth,
@@ -148,8 +158,8 @@ const batchAndLoadDocs =
if (docsIndex > -1) {
docs[docsIndex] = doc
}
}
}
})
}, Promise.resolve())
// Return docs array,
// which has now been injected with all fetched docs

View File

@@ -28,18 +28,18 @@ export const countOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'count',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'count',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },

View File

@@ -28,18 +28,18 @@ export const countVersionsOperation = async <TSlug extends CollectionSlug>(
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'countVersions',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'countVersions',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },

View File

@@ -78,8 +78,10 @@ export const createOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -88,8 +90,9 @@ export const createOperation = async <
operation: 'create',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
const {
autosave = false,
@@ -180,8 +183,10 @@ export const createOperation = async <
// beforeValidate - Collections
// /////////////////////////////////////
if (collectionConfig.hooks.beforeValidate?.length) {
for (const hook of collectionConfig.hooks.beforeValidate) {
await collectionConfig.hooks.beforeValidate.reduce(
async (priorHook: BeforeValidateHook | Promise<void>, hook: BeforeValidateHook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
@@ -191,26 +196,27 @@ export const createOperation = async <
originalDoc: duplicatedFromDoc,
req,
})) || data
}
}
},
Promise.resolve(),
)
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeChange?.length) {
for (const hook of collectionConfig.hooks.beforeChange) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'create',
originalDoc: duplicatedFromDoc,
req,
})) || data
}
}
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'create',
originalDoc: duplicatedFromDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
@@ -326,17 +332,17 @@ export const createOperation = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -357,8 +363,10 @@ export const createOperation = async <
// afterChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterChange?.length) {
for (const hook of collectionConfig.hooks.afterChange) {
await collectionConfig.hooks.afterChange.reduce(
async (priorHook: AfterChangeHook | Promise<void>, hook: AfterChangeHook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
@@ -368,8 +376,9 @@ export const createOperation = async <
previousDoc: {},
req: args.req,
})) || result
}
}
},
Promise.resolve(),
)
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -54,8 +54,10 @@ export const deleteOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -64,8 +66,9 @@ export const deleteOperation = async <
operation: 'delete',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
const {
collection: { config: collectionConfig },
@@ -144,16 +147,16 @@ export const deleteOperation = async <
// beforeDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeDelete?.length) {
for (const hook of collectionConfig.hooks.beforeDelete) {
await hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}
}
await collectionConfig.hooks.beforeDelete.reduce(async (priorHook, hook) => {
await priorHook
return hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}, Promise.resolve())
await deleteAssociatedFiles({
collectionConfig,
@@ -226,34 +229,34 @@ export const deleteOperation = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result || doc,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result || doc,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterDelete?.length) {
for (const hook of collectionConfig.hooks.afterDelete) {
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterDelete.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// 8. Return results

View File

@@ -48,8 +48,10 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
await args.collection.config.hooks.beforeOperation.reduce(
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
await priorHook
args =
(await hook({
args,
@@ -58,8 +60,9 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
operation: 'delete',
req: args.req,
})) || args
}
}
},
Promise.resolve(),
)
const {
id,
@@ -92,16 +95,16 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
// beforeDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeDelete?.length) {
for (const hook of collectionConfig.hooks.beforeDelete) {
await hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}
}
await collectionConfig.hooks.beforeDelete.reduce(async (priorHook, hook) => {
await priorHook
return hook({
id,
collection: collectionConfig,
context: req.context,
req,
})
}, Promise.resolve())
// /////////////////////////////////////
// Retrieve document
@@ -212,34 +215,34 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterDelete - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterDelete?.length) {
for (const hook of collectionConfig.hooks.afterDelete) {
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterDelete.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
id,
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -63,18 +63,18 @@ export const findOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
@@ -257,7 +257,9 @@ export const findOperation = async <
result.docs.map(async (doc) => {
let docRef = doc
for (const hook of collectionConfig.hooks.beforeRead) {
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
docRef =
(await hook({
collection: collectionConfig,
@@ -266,7 +268,7 @@ export const findOperation = async <
query: fullWhere,
req,
})) || docRef
}
}, Promise.resolve())
return docRef
}),
@@ -308,7 +310,9 @@ export const findOperation = async <
result.docs.map(async (doc) => {
let docRef = doc
for (const hook of collectionConfig.hooks.afterRead) {
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
docRef =
(await hook({
collection: collectionConfig,
@@ -318,7 +322,7 @@ export const findOperation = async <
query: fullWhere,
req,
})) || doc
}
}, Promise.resolve())
return docRef
}),

View File

@@ -54,18 +54,18 @@ export const findByIDOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}, Promise.resolve())
const {
id,
@@ -221,18 +221,18 @@ export const findByIDOperation = async <
// beforeRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeRead?.length) {
for (const hook of collectionConfig.hooks.beforeRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}
}
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -259,18 +259,18 @@ export const findByIDOperation = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
query: findOneArgs.where,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection

View File

@@ -101,18 +101,18 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// beforeRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeRead?.length) {
for (const hook of collectionConfig.hooks.beforeRead) {
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}
}
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -139,18 +139,18 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result.version,
query: fullWhere,
req,
})) || result.version
}, Promise.resolve())
// /////////////////////////////////////
// Return results

View File

@@ -96,19 +96,18 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
if (!docRef.version) {
;(docRef as any).version = {}
}
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
if (collectionConfig.hooks?.beforeRead?.length) {
for (const hook of collectionConfig.hooks.beforeRead) {
docRef.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef.version,
query: fullWhere,
req,
})) || docRef.version
}
}
docRef.version =
(await hook({
collection: collectionConfig,
context: req.context,
doc: docRef.version,
query: fullWhere,
req,
})) || docRef.version
}, Promise.resolve())
return docRef
}),
@@ -148,7 +147,9 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
result.docs.map(async (doc) => {
const docRef = doc
for (const hook of collectionConfig.hooks.afterRead) {
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
docRef.version =
(await hook({
collection: collectionConfig,
@@ -158,7 +159,7 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
query: fullWhere,
req,
})) || doc.version
}
}, Promise.resolve())
return docRef
}),

View File

@@ -165,17 +165,17 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -196,19 +196,19 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// afterChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterChange?.length) {
for (const hook of collectionConfig.hooks.afterChange) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: prevDocWithLocales,
req,
})) || result
}
}
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: prevDocWithLocales,
req,
})) || result
}, Promise.resolve())
return result
} catch (error: unknown) {

View File

@@ -62,18 +62,18 @@ export const updateOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },

View File

@@ -64,18 +64,18 @@ export const updateByIDOperation = async <
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}
}
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}, Promise.resolve())
if (args.publishSpecificLocale) {
args.req.locale = args.publishSpecificLocale

View File

@@ -171,19 +171,19 @@ export const updateDocument = async <
// beforeValidate - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeValidate?.length) {
for (const hook of collectionConfig.hooks.beforeValidate) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}
}
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// Write files to local storage
@@ -197,19 +197,19 @@ export const updateDocument = async <
// beforeChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeChange?.length) {
for (const hook of collectionConfig.hooks.beforeChange) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}
}
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
@@ -338,17 +338,17 @@ export const updateDocument = async <
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}
}
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -369,19 +369,19 @@ export const updateDocument = async <
// afterChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterChange?.length) {
for (const hook of collectionConfig.hooks.afterChange) {
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: originalDoc,
req,
})) || result
}
}
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: originalDoc,
req,
})) || result
}, Promise.resolve())
return result as TransformCollectionWithSelect<TSlug, TSelect>
}

View File

@@ -125,8 +125,10 @@ export const buildAfterOperation = async <
let newResult = result as OperationResult<TOperationGeneric, O>
if (args.collection.config.hooks?.afterOperation?.length) {
for (const hook of args.collection.config.hooks.afterOperation) {
await args.collection.config.hooks.afterOperation.reduce(
async (priorHook, hook: AfterOperationHook<TOperationGeneric>) => {
await priorHook
const hookResult = await hook({
args,
collection,
@@ -138,8 +140,9 @@ export const buildAfterOperation = async <
if (hookResult !== undefined) {
newResult = hookResult as OperationResult<TOperationGeneric, O>
}
}
}
},
Promise.resolve(),
)
return newResult
}

View File

@@ -9,10 +9,11 @@ import { sanitizeConfig } from './sanitize.js'
*/
export async function buildConfig(config: Config): Promise<SanitizedConfig> {
if (Array.isArray(config.plugins)) {
let configAfterPlugins = config
for (const plugin of config.plugins) {
configAfterPlugins = await plugin(configAfterPlugins)
}
const configAfterPlugins = await config.plugins.reduce(async (acc, plugin) => {
const configAfterPlugin = await acc
return plugin(configAfterPlugin)
}, Promise.resolve(config))
return await sanitizeConfig(configAfterPlugins)
}

View File

@@ -179,7 +179,10 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
}))
} else {
// is Locale[], so convert to string[] for localeCodes
config.localization.localeCodes = config.localization.locales.map((locale) => locale.code)
config.localization.localeCodes = config.localization.locales.reduce((locales, locale) => {
locales.push(locale.code)
return locales
}, [] as string[])
config.localization.locales = (
config.localization as LocalizationConfigWithLabels

View File

@@ -133,17 +133,17 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
// Execute before global hook
// /////////////////////////////////////
if (globalConfig.hooks?.beforeRead?.length) {
for (const hook of globalConfig.hooks.beforeRead) {
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}
}
await globalConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}, Promise.resolve())
// /////////////////////////////////////
// Execute globalType field if not selected
@@ -182,17 +182,17 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
// Execute after global hook
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
doc =
(await hook({
context: req.context,
doc,
global: globalConfig,
req,
})) || doc
}, Promise.resolve())
// /////////////////////////////////////
// Return results

View File

@@ -102,17 +102,17 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// beforeRead - Collection
// /////////////////////////////////////
if (globalConfig.hooks?.beforeRead?.length) {
for (const hook of globalConfig.hooks.beforeRead) {
result =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
req,
})) || result.version
}
}
await globalConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
req,
})) || result.version
}, Promise.resolve())
// /////////////////////////////////////
// afterRead - Fields
@@ -139,18 +139,18 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
result.version =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
query: findGlobalVersionsArgs.where,
req,
})) || result.version
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result.version =
(await hook({
context: req.context,
doc: result.version,
global: globalConfig,
query: findGlobalVersionsArgs.where,
req,
})) || result.version
}, Promise.resolve())
return result
} catch (error: unknown) {

View File

@@ -126,12 +126,15 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
result.docs = await Promise.all(
result = {
...result,
docs: await Promise.all(
result.docs.map(async (doc) => {
const docRef = doc
for (const hook of globalConfig.hooks.afterRead) {
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
docRef.version =
(await hook({
context: req.context,
@@ -141,11 +144,11 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
query: fullWhere,
req,
})) || doc.version
}
}, Promise.resolve())
return docRef
}),
)
),
}
// /////////////////////////////////////

View File

@@ -143,17 +143,17 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -174,18 +174,18 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
// afterChange - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterChange?.length) {
for (const hook of globalConfig.hooks.afterChange) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc,
req,
})) || result
}
}
await globalConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc,
req,
})) || result
}, Promise.resolve())
if (shouldCommit) {
await commitTransaction(req)

View File

@@ -168,35 +168,35 @@ export const updateOperation = async <
// beforeValidate - Global
// /////////////////////////////////////
if (globalConfig.hooks?.beforeValidate?.length) {
for (const hook of globalConfig.hooks.beforeValidate) {
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}
}
await globalConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Global
// /////////////////////////////////////
if (globalConfig.hooks?.beforeChange?.length) {
for (const hook of globalConfig.hooks.beforeChange) {
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}
}
await globalConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
context: req.context,
data,
global: globalConfig,
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
@@ -326,17 +326,17 @@ export const updateOperation = async <
// afterRead - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterRead?.length) {
for (const hook of globalConfig.hooks.afterRead) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}
}
await globalConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
@@ -357,18 +357,18 @@ export const updateOperation = async <
// afterChange - Global
// /////////////////////////////////////
if (globalConfig.hooks?.afterChange?.length) {
for (const hook of globalConfig.hooks.afterChange) {
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc: originalDoc,
req,
})) || result
}
}
await globalConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
context: req.context,
doc: result,
global: globalConfig,
previousDoc: originalDoc,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// Return results

View File

@@ -1374,7 +1374,6 @@ export { restoreVersionOperation as restoreVersionOperationGlobal } from './glob
export { updateOperation as updateOperationGlobal } from './globals/operations/update.js'
export type {
CollapsedPreferences,
ColumnPreference,
DocumentPreferences,
FieldsPreferences,
InsideFieldsPreferences,

View File

@@ -1,19 +0,0 @@
/**
* @todo remove this function and subsequent hooks in v4
* They are used to transform the old shape of `columnPreferences` to new shape
* i.e. ({ accessor: string, active: boolean })[] to ({ [accessor: string]: boolean })[]
* In v4 can we use the new shape directly
*/
export const migrateColumns = (value: Record<string, any>) => {
if (value && typeof value === 'object' && 'columns' in value && Array.isArray(value.columns)) {
value.columns = value.columns.map((col) => {
if ('accessor' in col) {
return { [col.accessor]: col.active }
}
return col
})
}
return value
}

View File

@@ -2,7 +2,6 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Access, Config } from '../config/types.js'
import { migrateColumns } from './migrateColumns.js'
import { deleteHandler } from './requestHandlers/delete.js'
import { findByIDHandler } from './requestHandlers/findOne.js'
import { updateHandler } from './requestHandlers/update.js'
@@ -77,14 +76,6 @@ const getPreferencesCollection = (config: Config): CollectionConfig => ({
{
name: 'value',
type: 'json',
/**
* @todo remove these hooks in v4
* See `migrateColumns` for more information
*/
hooks: {
afterRead: [({ value }) => migrateColumns(value)],
beforeValidate: [({ value }) => migrateColumns(value)],
},
validate: (value) => {
if (value) {
try {

View File

@@ -28,12 +28,8 @@ export type DocumentPreferences = {
fields: FieldsPreferences
}
export type ColumnPreference = {
[key: string]: boolean
}
export type ListPreferences = {
columns?: ColumnPreference[]
columns?: { accessor: string; active: boolean }[]
limit?: number
sort?: string
}

View File

@@ -1156,13 +1156,14 @@ export function configToJSONSchema(
)
: {}
const blocksDefinition: JSONSchema4 | undefined = {
type: 'object',
additionalProperties: false,
properties: {},
required: [],
}
let blocksDefinition: JSONSchema4 | undefined = undefined
if (config?.blocks?.length) {
blocksDefinition = {
type: 'object',
additionalProperties: false,
properties: {},
required: [],
}
for (const block of config.blocks) {
const blockFieldSchemas = fieldsToJSONSchema(
collectionIDFieldTypes,

View File

@@ -4,9 +4,12 @@ import { useModal } from '@faceless-ui/modal'
import React from 'react'
import { useTranslation } from '../../../providers/Translation/index.js'
import { ConfirmationModal } from '../../ConfirmationModal/index.js'
import { Button } from '../../Button/index.js'
import { FullscreenModal } from '../../FullscreenModal/index.js'
import { useBulkUpload } from '../index.js'
export const discardBulkUploadModalSlug = 'bulk-upload--discard-without-saving'
const baseClass = 'leave-without-saving'
export function DiscardWithoutSaving() {
const { t } = useTranslation()
@@ -23,14 +26,21 @@ export function DiscardWithoutSaving() {
}, [closeModal, drawerSlug])
return (
<ConfirmationModal
body={t('general:changesNotSaved')}
cancelLabel={t('general:stayOnThisPage')}
confirmLabel={t('general:leaveAnyway')}
heading={t('general:leaveWithoutSaving')}
modalSlug={discardBulkUploadModalSlug}
onCancel={onCancel}
onConfirm={onConfirm}
/>
<FullscreenModal className={baseClass} slug={discardBulkUploadModalSlug}>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__content`}>
<h1>{t('general:leaveWithoutSaving')}</h1>
<p>{t('general:changesNotSaved')}</p>
</div>
<div className={`${baseClass}__controls`}>
<Button buttonStyle="secondary" onClick={onCancel} size="large">
{t('general:stayOnThisPage')}
</Button>
<Button onClick={onConfirm} size="large">
{t('general:leaveAnyway')}
</Button>
</div>
</div>
</FullscreenModal>
)
}

View File

@@ -1,16 +1,16 @@
'use client'
import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import { useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React, { Fragment } from 'react'
import { useConfig } from '../../providers/Config/index.js'
import { useLocale, useLocaleLoading } from '../../providers/Locale/index.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { Popup, PopupList } from '../Popup/index.js'
import './index.scss'
import { LocalizerLabel } from './LocalizerLabel/index.js'
import './index.scss'
const baseClass = 'localizer'
@@ -21,10 +21,7 @@ export const Localizer: React.FC<{
const {
config: { localization },
} = useConfig()
const router = useRouter()
const { startRouteTransition } = useRouteTransition()
const searchParams = useSearchParams()
const { setLocaleIsLoading } = useLocaleLoading()
const { i18n } = useTranslation()
@@ -47,28 +44,17 @@ export const Localizer: React.FC<{
<PopupList.Button
active={locale.code === localeOption.code}
disabled={locale.code === localeOption.code}
href={qs.stringify(
{
...parseSearchParams(searchParams),
locale: localeOption.code,
},
{ addQueryPrefix: true },
)}
key={localeOption.code}
onClick={() => {
setLocaleIsLoading(true)
close()
// can't use `useSearchParams` here because it is stale due to `window.history.pushState` in `ListQueryProvider`
const searchParams = new URLSearchParams(window.location.search)
const url = qs.stringify(
{
...qs.parse(searchParams.toString(), {
depth: 10,
ignoreQueryPrefix: true,
}),
locale: localeOption.code,
},
{ addQueryPrefix: true },
)
startRouteTransition(() => {
router.push(url)
})
}}
>
{localeOptionLabel !== localeOption.code ? (

View File

@@ -222,7 +222,7 @@ export function PublishButton({ label: labelProp }: PublishButtonClientProps) {
)}
{localization && canPublish && (
<PopupList.ButtonGroup>
<PopupList.Button id="publish-locale" onClick={secondaryPublish}>
<PopupList.Button onClick={secondaryPublish}>
{secondaryLabel}
</PopupList.Button>
</PopupList.ButtonGroup>

View File

@@ -2,7 +2,6 @@
import type {
CollectionSlug,
Column,
ColumnPreference,
JoinFieldClient,
ListQuery,
PaginatedDocs,
@@ -26,6 +25,7 @@ import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { hoistQueryParamsToAnd } from '../../utilities/mergeListSearchAndWhere.js'
import { AnimateHeight } from '../AnimateHeight/index.js'
import './index.scss'
import { ColumnSelector } from '../ColumnSelector/index.js'
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
import { Popup, PopupList } from '../Popup/index.js'
@@ -33,7 +33,6 @@ import { RelationshipProvider } from '../Table/RelationshipProvider/index.js'
import { TableColumnsProvider } from '../TableColumns/index.js'
import { DrawerLink } from './cells/DrawerLink/index.js'
import { RelationshipTablePagination } from './Pagination.js'
import './index.scss'
const baseClass = 'relationship-table'
@@ -124,10 +123,11 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
newQuery.where = hoistQueryParamsToAnd(newQuery.where, filterOptions)
}
// map columns from string[] to ColumnPreference[]
const defaultColumns: ColumnPreference[] = field.admin.defaultColumns
// map columns from string[] to ListPreferences['columns']
const defaultColumns = field.admin.defaultColumns
? field.admin.defaultColumns.map((accessor) => ({
[accessor]: true,
accessor,
active: true,
}))
: undefined
@@ -137,7 +137,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
Table: NewTable,
} = await getTableState({
collectionSlug: relationTo,
columns: query?.columns || defaultColumns,
columns: defaultColumns,
docs,
enableRowSelections: false,
parent,
@@ -154,6 +154,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
[
field.defaultLimit,
field.defaultSort,
field.admin.defaultColumns,
collectionConfig?.admin?.pagination?.defaultLimit,
collectionConfig?.defaultSort,
query,
@@ -214,6 +215,8 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
[data?.docs, renderTable],
)
const preferenceKey = `${Array.isArray(relationTo) ? `${parent.collectionSlug}-${parent.joinPath}` : relationTo}-list`
const canCreate =
allowCreate !== false &&
permissions?.collections?.[Array.isArray(relationTo) ? relationTo[0] : relationTo]?.create
@@ -323,7 +326,6 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
{data?.docs && data.docs.length > 0 && (
<RelationshipProvider>
<ListQueryProvider
columns={columnState.map(({ accessor, active }) => ({ [accessor]: active }))}
data={data}
defaultLimit={
field.defaultLimit ?? collectionConfig?.admin?.pagination?.defaultLimit
@@ -334,9 +336,17 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
<TableColumnsProvider
collectionSlug={Array.isArray(relationTo) ? relationTo[0] : relationTo}
columnState={columnState}
docs={data.docs}
LinkedCellOverride={
<DrawerLink onDrawerDelete={onDrawerDelete} onDrawerSave={onDrawerSave} />
}
preferenceKey={preferenceKey}
renderRowTypes
setTable={setTable}
sortColumnProps={{
appearance: 'condensed',
}}
tableAppearance="condensed"
>
<AnimateHeight
className={`${baseClass}__columns`}

View File

@@ -1,12 +0,0 @@
@import '../../scss/styles.scss';
@layer payload-default {
.sort-row {
cursor: grab;
&__icon {
display: block;
width: min-content;
}
}
}

View File

@@ -1,16 +0,0 @@
'use client'
import React from 'react'
import { DragHandleIcon } from '../../icons/DragHandle/index.js'
import './index.scss'
const baseClass = 'sort-row'
export const SortRow = () => {
return (
<div className={baseClass} role="button" tabIndex={0}>
<DragHandleIcon className={`${baseClass}__icon`} />
</div>
)
}

View File

@@ -5,155 +5,55 @@ import type { Column } from 'payload'
import React from 'react'
import './index.scss'
import { useListQuery } from '../../providers/ListQuery/index.js'
import { DraggableSortableItem } from '../DraggableSortable/DraggableSortableItem/index.js'
import { DraggableSortable } from '../DraggableSortable/index.js'
const baseClass = 'table'
// if you change this, you need to change it in a couple of places where it's duplicated
const ORDER_FIELD_NAME = 'payload-order'
export type Props = {
readonly appearance?: 'condensed' | 'default'
readonly columns?: Column[]
readonly data: { [key: string]: unknown; id: string; 'payload-order': string }[]
readonly data: Record<string, unknown>[]
}
export const Table: React.FC<Props> = ({ appearance, columns, data: initialData }) => {
const { handleSortChange, query } = useListQuery()
const [data, setData] = React.useState(initialData)
// Force re-sort when data changes
React.useEffect(() => {
if (query.sort) {
void handleSortChange(query.sort as string).catch((error) => {
throw error
})
}
}, [data, handleSortChange, query.sort])
export const Table: React.FC<Props> = ({ appearance, columns, data }) => {
const activeColumns = columns?.filter((col) => col?.active)
if (!activeColumns || activeColumns.length === 0) {
return <div>No columns selected</div>
}
const handleDragEnd = async ({ moveFromIndex, moveToIndex }) => {
if (moveFromIndex === moveToIndex) {
return
}
const movedId = data[moveFromIndex].id
const newBeforeRow = moveToIndex > moveFromIndex ? data[moveToIndex] : data[moveToIndex - 1]
const newAfterRow = moveToIndex > moveFromIndex ? data[moveToIndex + 1] : data[moveToIndex]
// To debug:
// console.log(
// `moving ${data[moveFromIndex]?.text} between ${newBeforeRow?.text} and ${newAfterRow?.text}`,
// )
// Store the original data for rollback
const previousData = [...data]
// TODO: this optimistic update is not working (the table is not re-rendered)
// you can't debug it commenting the try block. Every move needs to be followed by
// a refresh of the page to see the changes.
setData((currentData) => {
const newData = [...currentData]
newData[moveFromIndex] = {
...newData[moveFromIndex],
[ORDER_FIELD_NAME]: `${newBeforeRow?.[ORDER_FIELD_NAME]}_pending`,
}
// move from index to moveToIndex
newData.splice(moveToIndex, 0, newData.splice(moveFromIndex, 1)[0])
return newData
})
try {
// Assuming we're in the context of a collection
const collectionSlug = window.location.pathname.split('/').filter(Boolean)[2]
const response = await fetch(`/api/${collectionSlug}/reorder`, {
body: JSON.stringify({
betweenIds: [newBeforeRow?.id, newAfterRow?.id],
docIds: [movedId],
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
if (!response.ok) {
throw new Error('Failed to reorder')
}
// no need to update the data here, the data is updated in the useListQuery provider
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error reordering:', error)
// Rollback to previous state if the request fails
setData(previousData)
// Optionally show an error notification
}
}
const rowIds = data.map((row) => row.id || String(Math.random()))
return (
<div
className={[baseClass, appearance && `${baseClass}--appearance-${appearance}`]
.filter(Boolean)
.join(' ')}
>
<DraggableSortable ids={rowIds} onDragEnd={handleDragEnd}>
<table cellPadding="0" cellSpacing="0">
<thead>
<tr>
{activeColumns.map((col, i) => (
<th id={`heading-${col.accessor}`} key={i}>
{col.Heading}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<DraggableSortableItem id={rowIds[rowIndex]} key={rowIds[rowIndex]}>
{({ attributes, listeners, setNodeRef, transform, transition }) => (
<tr
className={`row-${rowIndex + 1}`}
ref={setNodeRef}
style={{
transform,
transition,
}}
>
{activeColumns.map((col, colIndex) => {
const { accessor } = col
if (accessor === '_dragHandle') {
return (
<td className={`cell-${accessor}`} key={colIndex}>
<div {...attributes} {...listeners}>
{col.renderedCells[rowIndex]}
</div>
</td>
)
}
return (
<td className={`cell-${accessor}`} key={colIndex}>
{col.renderedCells[rowIndex]}
</td>
)
})}
</tr>
)}
</DraggableSortableItem>
<table cellPadding="0" cellSpacing="0">
<thead>
<tr>
{activeColumns.map((col, i) => (
<th id={`heading-${col.accessor}`} key={i}>
{col.Heading}
</th>
))}
</tbody>
</table>
</DraggableSortable>
</tr>
</thead>
<tbody>
{data &&
data.map((row, rowIndex) => (
<tr className={`row-${rowIndex + 1}`} key={rowIndex}>
{activeColumns.map((col, colIndex) => {
const { accessor } = col
return (
<td className={`cell-${accessor}`} key={colIndex}>
{col.renderedCells[rowIndex]}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -4,10 +4,10 @@ import type {
ClientComponentProps,
ClientField,
Column,
ColumnPreference,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Field,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -39,8 +39,8 @@ type Args = {
beforeRows?: Column[]
clientCollectionConfig: ClientCollectionConfig
collectionConfig: SanitizedCollectionConfig
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
customCellProps: DefaultCellComponentProps['customCellProps']
docs: PaginatedDocs['docs']
enableRowSelections: boolean
@@ -99,10 +99,10 @@ export const buildColumnState = (args: Args): Column[] => {
const sortTo = columnPreferences || columns
const sortFieldMap = (fieldMap, sortTo: ColumnPreference[]) =>
const sortFieldMap = (fieldMap, sortTo) =>
fieldMap?.sort((a, b) => {
const aIndex = sortTo.findIndex((column) => 'name' in a && a.name in column)
const bIndex = sortTo.findIndex((column) => 'name' in b && b.name in column)
const aIndex = sortTo.findIndex((column) => 'name' in a && column.accessor === a.name)
const bIndex = sortTo.findIndex((column) => 'name' in b && column.accessor === b.name)
if (aIndex === -1 && bIndex === -1) {
return 0
@@ -136,12 +136,18 @@ export const buildColumnState = (args: Args): Column[] => {
(f) => 'name' in field && 'name' in f && f.name === field.name,
)
const columnPreference = columnPreferences?.find(
(preference) => field && 'name' in field && preference.accessor === field.name,
)
let active = false
if (columnPreferences) {
active = 'name' in field && columnPreferences?.some((col) => col?.[field.name])
if (columnPreference) {
active = columnPreference.active
} else if (columns && Array.isArray(columns) && columns.length > 0) {
active = 'name' in field && columns.some((col) => col?.[field.name])
active = columns.find(
(column) => field && 'name' in field && column.accessor === field.name,
)?.active
} else if (activeColumnsIndices.length < 4) {
active = true
}

View File

@@ -4,10 +4,10 @@ import type { I18nClient } from '@payloadcms/translations'
import type {
ClientField,
Column,
ColumnPreference,
DefaultCellComponentProps,
DefaultServerCellComponentProps,
Field,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -36,8 +36,8 @@ import { filterFields } from './filterFields.js'
type Args = {
beforeRows?: Column[]
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
customCellProps: DefaultCellComponentProps['customCellProps']
docs: PaginatedDocs['docs']
enableRowSelections: boolean
@@ -92,8 +92,8 @@ export const buildPolymorphicColumnState = (args: Args): Column[] => {
const sortFieldMap = (fieldMap, sortTo) =>
fieldMap?.sort((a, b) => {
const aIndex = sortTo.findIndex((column) => 'name' in a && a.name in column)
const bIndex = sortTo.findIndex((column) => 'name' in b && b.name in column)
const aIndex = sortTo.findIndex((column) => 'name' in a && column.accessor === a.name)
const bIndex = sortTo.findIndex((column) => 'name' in b && column.accessor === b.name)
if (aIndex === -1 && bIndex === -1) {
return 0
@@ -127,12 +127,18 @@ export const buildPolymorphicColumnState = (args: Args): Column[] => {
(f) => 'name' in field && 'name' in f && f.name === field.name,
)
const columnPreference = columnPreferences?.find(
(preference) => field && 'name' in field && preference.accessor === field.name,
)
let active = false
if (columnPreferences) {
active = 'name' in field && columnPreferences?.some((col) => col?.[field.name])
if (columnPreference) {
active = columnPreference.active
} else if (columns && Array.isArray(columns) && columns.length > 0) {
active = 'name' in field && columns.some((col) => col?.[field.name])
active = columns.find(
(column) => field && 'name' in field && column.accessor === field.name,
)?.active
} else if (activeColumnsIndices.length < 4) {
active = true
}

View File

@@ -1,7 +0,0 @@
import { createContext, useContext } from 'react'
import type { ITableColumns } from './types.js'
export const TableColumnContext = createContext<ITableColumns>({} as ITableColumns)
export const useTableColumns = (): ITableColumns => useContext(TableColumnContext)

View File

@@ -1,11 +1,11 @@
import type { ClientField, CollectionConfig, ColumnPreference, Field } from 'payload'
import type { ClientField, CollectionConfig, Field, ListPreferences } from 'payload'
import { fieldAffectsData } from 'payload/shared'
const getRemainingColumns = <T extends ClientField[] | Field[]>(
fields: T,
useAsTitle: string,
): ColumnPreference[] =>
): ListPreferences['columns'] =>
fields?.reduce((remaining, field) => {
if (fieldAffectsData(field) && field.name === useAsTitle) {
return remaining
@@ -40,7 +40,7 @@ export const getInitialColumns = <T extends ClientField[] | Field[]>(
fields: T,
useAsTitle: CollectionConfig['admin']['useAsTitle'],
defaultColumns: CollectionConfig['admin']['defaultColumns'],
): ColumnPreference[] => {
): ListPreferences['columns'] => {
let initialColumns = []
if (Array.isArray(defaultColumns) && defaultColumns.length >= 1) {
@@ -57,6 +57,7 @@ export const getInitialColumns = <T extends ClientField[] | Field[]>(
}
return initialColumns.map((column) => ({
[column]: true,
accessor: column,
active: true,
}))
}

View File

@@ -1,99 +1,293 @@
'use client'
import type { Column } from 'payload'
import type { Column, ListPreferences, SanitizedCollectionConfig } from 'payload'
import React, { startTransition, useCallback } from 'react'
import React, { createContext, useCallback, useContext, useEffect } from 'react'
import type { TableColumnsProviderProps } from './types.js'
import type { SortColumnProps } from '../SortColumn/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useListQuery } from '../../providers/ListQuery/index.js'
import { TableColumnContext } from './context.js'
import { usePreferences } from '../../providers/Preferences/index.js'
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js'
export { useTableColumns } from './context.js'
export interface ITableColumns {
columns: Column[]
LinkedCellOverride?: React.ReactNode
moveColumn: (args: { fromIndex: number; toIndex: number }) => Promise<void>
resetColumnsState: () => Promise<void>
setActiveColumns: (columns: string[]) => Promise<void>
toggleColumn: (column: string) => Promise<void>
}
export const TableColumnsProvider: React.FC<TableColumnsProviderProps> = ({
export const TableColumnContext = createContext<ITableColumns>({} as ITableColumns)
export const useTableColumns = (): ITableColumns => useContext(TableColumnContext)
type Props = {
readonly children: React.ReactNode
readonly collectionSlug: string | string[]
readonly columnState: Column[]
readonly docs: any[]
readonly enableRowSelections?: boolean
readonly LinkedCellOverride?: React.ReactNode
readonly listPreferences?: ListPreferences
readonly preferenceKey: string
readonly renderRowTypes?: boolean
readonly setTable: (Table: React.ReactNode) => void
readonly sortColumnProps?: Partial<SortColumnProps>
readonly tableAppearance?: 'condensed' | 'default'
}
// strip out Heading, Label, and renderedCells properties, they cannot be sent to the server
const sanitizeColumns = (columns: Column[]) => {
return columns.map(({ accessor, active }) => ({
accessor,
active,
}))
}
export const TableColumnsProvider: React.FC<Props> = ({
children,
collectionSlug,
columnState: columnStateFromProps,
columnState,
docs,
enableRowSelections,
LinkedCellOverride,
listPreferences,
preferenceKey,
renderRowTypes,
setTable,
sortColumnProps,
tableAppearance,
}) => {
const { getEntityConfig } = useConfig()
const { query: currentQuery, refineListData } = useListQuery()
const { admin: { defaultColumns } = {} } = getEntityConfig({
const { getTableState } = useServerFunctions()
const { admin: { defaultColumns, useAsTitle } = {}, fields } = getEntityConfig({
collectionSlug,
})
const [columnState, setOptimisticColumnState] = React.useOptimistic(
columnStateFromProps,
(state, action: Column[]) => action,
const prevCollection = React.useRef<SanitizedCollectionConfig['slug']>(
Array.isArray(collectionSlug) ? collectionSlug[0] : collectionSlug,
)
const { getPreference } = usePreferences()
const [tableColumns, setTableColumns] = React.useState(columnState)
const abortTableStateRef = React.useRef<AbortController>(null)
const abortToggleColumnRef = React.useRef<AbortController>(null)
const moveColumn = useCallback(
async (args: { fromIndex: number; toIndex: number }) => {
const controller = handleAbortRef(abortTableStateRef)
const { fromIndex, toIndex } = args
const withMovedColumn = [...tableColumns]
const [columnToMove] = withMovedColumn.splice(fromIndex, 1)
withMovedColumn.splice(toIndex, 0, columnToMove)
setTableColumns(withMovedColumn)
const result = await getTableState({
collectionSlug,
columns: sanitizeColumns(withMovedColumn),
docs,
enableRowSelections,
renderRowTypes,
signal: controller.signal,
tableAppearance,
})
if (result) {
setTableColumns(result.state)
setTable(result.Table)
}
abortTableStateRef.current = null
},
[
tableColumns,
collectionSlug,
docs,
getTableState,
setTable,
enableRowSelections,
renderRowTypes,
tableAppearance,
],
)
const toggleColumn = useCallback(
async (column: string) => {
const newColumnState = (columnState || []).map((col) => {
if (col.accessor === column) {
return { ...col, active: !col.active }
}
return col
})
const controller = handleAbortRef(abortToggleColumnRef)
startTransition(() => {
setOptimisticColumnState(newColumnState)
})
await refineListData({
columns: newColumnState.map((col) => ({ [col.accessor]: col.active })),
})
},
[refineListData, columnState, setOptimisticColumnState],
)
const moveColumn = useCallback(
async (args: { fromIndex: number; toIndex: number }) => {
const { fromIndex, toIndex } = args
const newColumnState = [...(columnState || [])]
const [columnToMove] = newColumnState.splice(fromIndex, 1)
newColumnState.splice(toIndex, 0, columnToMove)
startTransition(() => {
setOptimisticColumnState(newColumnState)
})
await refineListData({
columns: newColumnState.map((col) => ({ [col.accessor]: col.active })),
})
},
[columnState, refineListData, setOptimisticColumnState],
)
const setActiveColumns = useCallback(
async (columns: string[]) => {
const newColumnState = currentQuery.columns
columns.forEach((colName) => {
const colIndex = newColumnState.findIndex((c) => colName in c)
if (colIndex !== undefined) {
newColumnState[colIndex] = {
[colName]: true,
const { newColumnState, toggledColumns } = tableColumns.reduce<{
newColumnState: Column[]
toggledColumns: Pick<Column, 'accessor' | 'active'>[]
}>(
(acc, col) => {
if (col.accessor === column) {
acc.newColumnState.push({
...col,
accessor: col.accessor,
active: !col.active,
})
acc.toggledColumns.push({
accessor: col.accessor,
active: !col.active,
})
} else {
acc.newColumnState.push(col)
acc.toggledColumns.push({
accessor: col.accessor,
active: col.active,
})
}
}
return acc
},
{ newColumnState: [], toggledColumns: [] },
)
setTableColumns(newColumnState)
const result = await getTableState({
collectionSlug,
columns: toggledColumns,
docs,
enableRowSelections,
renderRowTypes,
signal: controller.signal,
tableAppearance,
})
await refineListData({ columns: newColumnState })
if (result) {
setTableColumns(result.state)
setTable(result.Table)
}
abortToggleColumnRef.current = null
},
[currentQuery, refineListData],
[
tableColumns,
getTableState,
setTable,
collectionSlug,
docs,
enableRowSelections,
renderRowTypes,
tableAppearance,
],
)
const setActiveColumns = React.useCallback(
async (activeColumnAccessors: string[]) => {
const activeColumns: Pick<Column, 'accessor' | 'active'>[] = tableColumns
.map((col) => {
return {
accessor: col.accessor,
active: activeColumnAccessors.includes(col.accessor),
}
})
.sort((first, second) => {
const indexOfFirst = activeColumnAccessors.indexOf(first.accessor)
const indexOfSecond = activeColumnAccessors.indexOf(second.accessor)
if (indexOfFirst === -1 || indexOfSecond === -1) {
return 0
}
return indexOfFirst > indexOfSecond ? 1 : -1
})
const { state: columnState, Table } = await getTableState({
collectionSlug,
columns: activeColumns,
docs,
enableRowSelections,
renderRowTypes,
tableAppearance,
})
setTableColumns(columnState)
setTable(Table)
},
[
tableColumns,
getTableState,
setTable,
collectionSlug,
docs,
enableRowSelections,
renderRowTypes,
tableAppearance,
],
)
const resetColumnsState = React.useCallback(async () => {
await setActiveColumns(defaultColumns)
}, [defaultColumns, setActiveColumns])
// //////////////////////////////////////////////
// Get preferences on collection change (drawers)
// //////////////////////////////////////////////
React.useEffect(() => {
const sync = async () => {
const defaultCollection = Array.isArray(collectionSlug) ? collectionSlug[0] : collectionSlug
const collectionHasChanged = prevCollection.current !== defaultCollection
if (collectionHasChanged || !listPreferences) {
const currentPreferences = await getPreference<{
columns: ListPreferences['columns']
}>(preferenceKey)
prevCollection.current = defaultCollection
if (currentPreferences?.columns) {
// setTableColumns()
// buildColumnState({
// beforeRows,
// columnPreferences: currentPreferences?.columns,
// columns: initialColumns,
// enableRowSelections,
// fields,
// sortColumnProps,
// useAsTitle,
// }),
}
}
}
void sync()
}, [
preferenceKey,
getPreference,
collectionSlug,
fields,
defaultColumns,
useAsTitle,
listPreferences,
enableRowSelections,
sortColumnProps,
])
useEffect(() => {
setTableColumns(columnState)
}, [columnState])
useEffect(() => {
const abortTableState = abortTableStateRef.current
return () => {
abortAndIgnore(abortTableState)
}
}, [])
return (
<TableColumnContext.Provider
value={{
columns: columnState,
columns: tableColumns,
LinkedCellOverride,
moveColumn,
resetColumnsState,

View File

@@ -1,51 +0,0 @@
import type { Column, ListPreferences } from 'payload'
import type { SortColumnProps } from '../SortColumn/index.js'
export interface ITableColumns {
columns: Column[]
LinkedCellOverride?: React.ReactNode
moveColumn: (args: { fromIndex: number; toIndex: number }) => Promise<void>
resetColumnsState: () => Promise<void>
setActiveColumns: (columns: string[]) => Promise<void>
toggleColumn: (column: string) => Promise<void>
}
export type TableColumnsProviderProps = {
readonly children: React.ReactNode
readonly collectionSlug: string | string[]
readonly columnState: Column[]
/**
* @deprecated
*/
readonly docs?: any[]
/**
* @deprecated
*/
readonly enableRowSelections?: boolean
readonly LinkedCellOverride?: React.ReactNode
/**
* @deprecated
*/
readonly listPreferences?: ListPreferences
/**
* @deprecated
*/
readonly preferenceKey?: string
/**
* @deprecated
*/
readonly renderRowTypes?: boolean
/**
* @deprecated
*/
readonly setTable?: (Table: React.ReactNode) => void
/**
* @deprecated
*/
readonly sortColumnProps?: Partial<SortColumnProps>
/**
* @deprecated
*/
readonly tableAppearance?: 'condensed' | 'default'
}

View File

@@ -1,7 +1,7 @@
'use client'
import React, { useCallback, useEffect, useState } from 'react'
import type { AddCondition, ReducedField, RemoveCondition, UpdateCondition } from '../types.js'
import type { AddCondition, ReducedField, UpdateCondition } from '../types.js'
export type Props = {
readonly addCondition: AddCondition
@@ -11,7 +11,7 @@ export type Props = {
readonly operator: Operator
readonly orIndex: number
readonly reducedFields: ReducedField[]
readonly removeCondition: RemoveCondition
readonly removeCondition: ({ andIndex, orIndex }: { andIndex: number; orIndex: number }) => void
readonly RenderedFilter: React.ReactNode
readonly updateCondition: UpdateCondition
readonly value: string
@@ -67,9 +67,9 @@ export const Condition: React.FC<Props> = (props) => {
valueOptions = reducedField.field.options
}
const updateValue = useEffectEvent(async (debouncedValue) => {
const updateValue = useEffectEvent((debouncedValue) => {
if (operator) {
await updateCondition({
updateCondition({
andIndex,
field: reducedField,
operator,
@@ -80,7 +80,7 @@ export const Condition: React.FC<Props> = (props) => {
})
useEffect(() => {
void updateValue(debouncedValue)
updateValue(debouncedValue)
}, [debouncedValue])
const disabled =
@@ -88,9 +88,9 @@ export const Condition: React.FC<Props> = (props) => {
reducedField?.field?.admin?.disableListFilter
const handleFieldChange = useCallback(
async (field: Option<string>) => {
(field: Option<string>) => {
setInternalValue(undefined)
await updateCondition({
updateCondition({
andIndex,
field: reducedFields.find((option) => option.value === field.value),
operator,
@@ -102,8 +102,8 @@ export const Condition: React.FC<Props> = (props) => {
)
const handleOperatorChange = useCallback(
async (operator: Option<Operator>) => {
await updateCondition({
(operator: Option<Operator>) => {
updateCondition({
andIndex,
field: reducedField,
operator: operator.value,

View File

@@ -4,9 +4,8 @@ import type { Operator, Where } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React, { useMemo } from 'react'
import type { AddCondition, RemoveCondition, UpdateCondition, WhereBuilderProps } from './types.js'
import type { AddCondition, UpdateCondition, WhereBuilderProps } from './types.js'
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
import { useListQuery } from '../../providers/ListQuery/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { Button } from '../Button/index.js'
@@ -32,6 +31,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
const reducedFields = useMemo(() => reduceFields({ fields, i18n }), [fields, i18n])
const { handleWhereChange, query } = useListQuery()
const [shouldUpdateQuery, setShouldUpdateQuery] = React.useState(false)
const [conditions, setConditions] = React.useState<Where[]>(() => {
const whereFromSearch = query.where
@@ -55,7 +55,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
})
const addCondition: AddCondition = React.useCallback(
async ({ andIndex, field, orIndex, relation }) => {
({ andIndex, field, orIndex, relation }) => {
const newConditions = [...conditions]
const defaultOperator = fieldTypes[field.field.type].operators[0].value
@@ -79,13 +79,12 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
}
setConditions(newConditions)
await handleWhereChange({ or: conditions })
},
[conditions, handleWhereChange],
[conditions],
)
const updateCondition: UpdateCondition = React.useCallback(
async ({ andIndex, field, operator: incomingOperator, orIndex, value: valueArg }) => {
({ andIndex, field, operator: incomingOperator, orIndex, value: valueArg }) => {
const existingRowCondition = conditions[orIndex].and[andIndex]
const defaults = fieldTypes[field.field.type]
@@ -102,14 +101,14 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
newConditions[orIndex].and[andIndex] = newRowCondition
setConditions(newConditions)
await handleWhereChange({ or: conditions })
setShouldUpdateQuery(true)
}
},
[conditions, handleWhereChange],
[conditions],
)
const removeCondition: RemoveCondition = React.useCallback(
async ({ andIndex, orIndex }) => {
const removeCondition = React.useCallback(
({ andIndex, orIndex }) => {
const newConditions = [...conditions]
newConditions[orIndex].and.splice(andIndex, 1)
@@ -118,11 +117,21 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
}
setConditions(newConditions)
await handleWhereChange({ or: conditions })
setShouldUpdateQuery(true)
},
[conditions, handleWhereChange],
[conditions],
)
React.useEffect(() => {
if (shouldUpdateQuery) {
async function handleChange() {
await handleWhereChange({ or: conditions })
setShouldUpdateQuery(false)
}
void handleChange()
}
}, [conditions, handleWhereChange, shouldUpdateQuery])
return (
<div className={baseClass}>
{conditions.length > 0 && (
@@ -180,8 +189,8 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
icon="plus"
iconPosition="left"
iconStyle="with-border"
onClick={async () => {
await addCondition({
onClick={() => {
addCondition({
andIndex: 0,
field: reducedFields[0],
orIndex: conditions.length,
@@ -202,9 +211,9 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
icon="plus"
iconPosition="left"
iconStyle="with-border"
onClick={async () => {
onClick={() => {
if (reducedFields.length > 0) {
await addCondition({
addCondition({
andIndex: 0,
field: reducedFields.find((field) => !field.field.admin?.disableListFilter),
orIndex: conditions.length,

View File

@@ -65,7 +65,7 @@ export type AddCondition = ({
field: ReducedField
orIndex: number
relation: 'and' | 'or'
}) => Promise<void> | void
}) => void
export type UpdateCondition = ({
andIndex,
@@ -79,12 +79,4 @@ export type UpdateCondition = ({
operator: string
orIndex: number
value: string
}) => Promise<void> | void
export type RemoveCondition = ({
andIndex,
orIndex,
}: {
andIndex: number
orIndex: number
}) => Promise<void> | void
}) => void

View File

@@ -10,10 +10,10 @@ import { useField } from '../../forms/useField/index.js'
import { withCondition } from '../../forms/withCondition/index.js'
import { FieldDescription } from '../FieldDescription/index.js'
import { FieldError } from '../FieldError/index.js'
import './index.scss'
import { FieldLabel } from '../FieldLabel/index.js'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import { fieldBaseClass } from '../shared/index.js'
import './index.scss'
const baseClass = 'json-field'
@@ -31,9 +31,10 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
readOnly,
validate,
} = props
const [stringValue, setStringValue] = useState<string>()
const [jsonError, setJsonError] = useState<string>()
const inputChangeFromRef = React.useRef<'system' | 'user'>('system')
const [editorKey, setEditorKey] = useState<string>('')
const [hasLoadedValue, setHasLoadedValue] = useState(false)
const memoizedValidate = useCallback(
(value, options) => {
@@ -55,12 +56,6 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
validate: memoizedValidate,
})
const [initialStringValue, setInitialStringValue] = useState<string | undefined>(() =>
(value || initialValue) !== undefined
? JSON.stringify(value ?? initialValue, null, 2)
: undefined,
)
const handleMount = useCallback<OnMount>(
(editor, monaco) => {
if (!jsonSchema) {
@@ -93,7 +88,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
if (readOnly) {
return
}
inputChangeFromRef.current = 'user'
setStringValue(val)
try {
setValue(val ? JSON.parse(val) : null)
@@ -103,21 +98,20 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
setJsonError(e)
}
},
[readOnly, setValue],
[readOnly, setValue, setStringValue],
)
useEffect(() => {
if (inputChangeFromRef.current === 'system') {
setInitialStringValue(
(value || initialValue) !== undefined
? JSON.stringify(value ?? initialValue, null, 2)
: undefined,
)
setEditorKey(new Date().toString())
if (hasLoadedValue || value === undefined) {
return
}
inputChangeFromRef.current = 'system'
}, [initialValue, value])
setStringValue(
value || initialValue ? JSON.stringify(value ? value : initialValue, null, 2) : '',
)
setHasLoadedValue(true)
}, [initialValue, value, hasLoadedValue])
const styles = useMemo(() => mergeFieldStyles(field), [field])
@@ -148,16 +142,12 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
{BeforeInput}
<CodeEditor
defaultLanguage="json"
key={editorKey}
maxHeight={maxHeight}
onChange={handleChange}
onMount={handleMount}
options={editorOptions}
readOnly={readOnly}
value={initialStringValue}
wrapperProps={{
id: `field-${path?.replace(/\./g, '__')}`,
}}
value={stringValue}
/>
{AfterInput}
</div>

View File

@@ -1,7 +0,0 @@
import { createContext, useContext } from 'react'
import type { IListQueryContext } from './types.js'
export const ListQueryContext = createContext({} as IListQueryContext)
export const useListQuery = (): IListQueryContext => useContext(ListQueryContext)

View File

@@ -1,50 +1,57 @@
'use client'
import type { ColumnPreference, ListQuery, Where } from 'payload'
import type { ListQuery, PaginatedDocs, Sort, Where } from 'payload'
import { useRouter, useSearchParams } from 'next/navigation.js'
import { isNumber } from 'payload/shared'
import * as qs from 'qs-esm'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { ListQueryProps } from './types.js'
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js'
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { ListQueryContext } from './context.js'
export { useListQuery } from './context.js'
type ContextHandlers = {
handlePageChange?: (page: number) => Promise<void>
handlePerPageChange?: (limit: number) => Promise<void>
handleSearchChange?: (search: string) => Promise<void>
handleSortChange?: (sort: string) => Promise<void>
handleWhereChange?: (where: Where) => Promise<void>
}
export type ListQueryProps = {
readonly children: React.ReactNode
readonly collectionSlug?: string
readonly data: PaginatedDocs
readonly defaultLimit?: number
readonly defaultSort?: Sort
readonly modifySearchParams?: boolean
readonly onQueryChange?: (query: ListQuery) => void
readonly preferenceKey?: string
}
export type ListQueryContext = {
data: PaginatedDocs
defaultLimit?: number
defaultSort?: Sort
query: ListQuery
refineListData: (args: ListQuery) => Promise<void>
} & ContextHandlers
const Context = createContext({} as ListQueryContext)
export const useListQuery = (): ListQueryContext => useContext(Context)
export const ListQueryProvider: React.FC<ListQueryProps> = ({
children,
columns,
data,
defaultLimit,
defaultSort,
listPreferences,
modifySearchParams,
onQueryChange: onQueryChangeFromProps,
}) => {
'use no memo'
const router = useRouter()
const rawSearchParams = useSearchParams()
const { startRouteTransition } = useRouteTransition()
const searchParams = useMemo<ListQuery>(() => {
const parsed = parseSearchParams(rawSearchParams)
const result: ListQuery = parsed
if (parsed.columns) {
try {
result.columns = JSON.parse(parsed.columns as string) as ColumnPreference[]
} catch (error) {
console.error('Error parsing columns from URL:', error) // eslint-disable-line no-console
}
}
return result
}, [rawSearchParams])
const searchParams = useMemo(() => parseSearchParams(rawSearchParams), [rawSearchParams])
const { onQueryChange } = useListDrawerContext()
@@ -56,6 +63,8 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
}
})
const currentQueryRef = React.useRef(currentQuery)
// If the search params change externally, update the current query
useEffect(() => {
if (modifySearchParams) {
@@ -73,7 +82,6 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
}
const newQuery: ListQuery = {
columns: 'columns' in query ? query.columns : currentQuery.columns,
limit: 'limit' in query ? query.limit : (currentQuery?.limit ?? String(defaultLimit)),
page,
search: 'search' in query ? query.search : currentQuery?.search,
@@ -82,14 +90,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
}
if (modifySearchParams) {
startRouteTransition(() =>
router.replace(
`${qs.stringify(
{ ...newQuery, columns: JSON.stringify(newQuery.columns) },
{ addQueryPrefix: true },
)}`,
),
)
router.replace(`${qs.stringify(newQuery, { addQueryPrefix: true })}`)
} else if (
typeof onQueryChange === 'function' ||
typeof onQueryChangeFromProps === 'function'
@@ -101,13 +102,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
setCurrentQuery(newQuery)
},
[
currentQuery?.columns,
currentQuery?.limit,
currentQuery?.page,
currentQuery?.search,
currentQuery?.sort,
currentQuery?.where,
startRouteTransition,
currentQuery,
defaultLimit,
defaultSort,
modifySearchParams,
@@ -153,50 +148,34 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
[refineListData],
)
const syncQuery = useEffectEvent(() => {
let shouldUpdateQueryString = false
const newQuery = { ...(currentQuery || {}) }
// Allow the URL to override the default limit
if (isNumber(defaultLimit) && !('limit' in currentQuery)) {
newQuery.limit = String(defaultLimit)
shouldUpdateQueryString = true
}
// Allow the URL to override the default sort
if (defaultSort && !('sort' in currentQuery)) {
newQuery.sort = defaultSort
shouldUpdateQueryString = true
}
// Only modify columns if they originated from preferences
// We can assume they did if `listPreferences.columns` is defined
if (columns && listPreferences?.columns && !('columns' in currentQuery)) {
newQuery.columns = columns
shouldUpdateQueryString = true
}
if (shouldUpdateQueryString) {
setCurrentQuery(newQuery)
// Do not use router.replace here to avoid re-rendering on initial load
window.history.replaceState(
null,
'',
`?${qs.stringify({ ...newQuery, columns: JSON.stringify(newQuery.columns) })}`,
)
}
})
// If `defaultLimit` or `defaultSort` are updated externally, update the query
// I.e. when HMR runs, these properties may be different
useEffect(() => {
if (modifySearchParams) {
syncQuery()
let shouldUpdateQueryString = false
const newQuery = { ...(currentQueryRef.current || {}) }
// Allow the URL to override the default limit
if (isNumber(defaultLimit) && !('limit' in currentQueryRef.current)) {
newQuery.limit = String(defaultLimit)
shouldUpdateQueryString = true
}
// Allow the URL to override the default sort
if (defaultSort && !('sort' in currentQueryRef.current)) {
newQuery.sort = defaultSort
shouldUpdateQueryString = true
}
if (shouldUpdateQueryString) {
setCurrentQuery(newQuery)
router.replace(`${qs.stringify(newQuery, { addQueryPrefix: true })}`)
}
}
}, [defaultSort, defaultLimit, modifySearchParams, columns])
}, [defaultSort, defaultLimit, router, modifySearchParams])
return (
<ListQueryContext.Provider
<Context.Provider
value={{
data,
handlePageChange,
@@ -209,6 +188,6 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
}}
>
{children}
</ListQueryContext.Provider>
</Context.Provider>
)
}

View File

@@ -1,42 +0,0 @@
import type {
ColumnPreference,
ListPreferences,
ListQuery,
PaginatedDocs,
Sort,
Where,
} from 'payload'
type ContextHandlers = {
handlePageChange?: (page: number) => Promise<void>
handlePerPageChange?: (limit: number) => Promise<void>
handleSearchChange?: (search: string) => Promise<void>
handleSortChange?: (sort: string) => Promise<void>
handleWhereChange?: (where: Where) => Promise<void>
}
export type OnListQueryChange = (query: ListQuery) => void
export type ListQueryProps = {
readonly children: React.ReactNode
readonly collectionSlug?: string
readonly columns?: ColumnPreference[]
readonly data: PaginatedDocs
readonly defaultLimit?: number
readonly defaultSort?: Sort
readonly listPreferences?: ListPreferences
readonly modifySearchParams?: boolean
readonly onQueryChange?: OnListQueryChange
/**
* @deprecated
*/
readonly preferenceKey?: string
}
export type IListQueryContext = {
data: PaginatedDocs
defaultLimit?: number
defaultSort?: Sort
query: ListQuery
refineListData: (args: ListQuery) => Promise<void>
} & ContextHandlers

View File

@@ -3,10 +3,9 @@ import type {
ClientConfig,
ClientField,
CollectionConfig,
Column,
ColumnPreference,
Field,
ImportMap,
ListPreferences,
PaginatedDocs,
Payload,
SanitizedCollectionConfig,
@@ -15,12 +14,15 @@ import type {
import { getTranslation, type I18nClient } from '@payloadcms/translations'
import { fieldAffectsData, fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
// eslint-disable-next-line payload/no-imports-from-exports-dir
import type { Column } from '../exports/client/index.js'
import { RenderServerComponent } from '../elements/RenderServerComponent/index.js'
import { SortRow } from '../elements/SortRow/index.js'
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
import { buildPolymorphicColumnState } from '../elements/TableColumns/buildPolymorphicColumnState.js'
import { filterFields } from '../elements/TableColumns/filterFields.js'
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
// eslint-disable-next-line payload/no-imports-from-exports-dir
import { Pill, SelectAll, SelectRow, Table } from '../exports/client/index.js'
@@ -49,9 +51,6 @@ export const renderFilters = (
new Map() as Map<string, React.ReactNode>,
)
// Add drag handle column if documents have payload-order field
export const ORDER_FIELD_NAME = 'payload-order'
export const renderTable = ({
clientCollectionConfig,
clientConfig,
@@ -72,8 +71,8 @@ export const renderTable = ({
clientConfig?: ClientConfig
collectionConfig?: SanitizedCollectionConfig
collections?: string[]
columnPreferences: ColumnPreference[]
columns?: ColumnPreference[]
columnPreferences: ListPreferences['columns']
columns?: ListPreferences['columns']
customCellProps?: Record<string, any>
docs: PaginatedDocs['docs']
drawerSlug?: string
@@ -110,7 +109,7 @@ export const renderTable = ({
const columns = columnsFromArgs
? columnsFromArgs?.filter((column) =>
flattenTopLevelFields(fields, true)?.some(
(field) => 'name' in field && column[field.name],
(field) => 'name' in field && field.name === column.accessor,
),
)
: getInitialColumns(fields, useAsTitle, [])
@@ -131,7 +130,7 @@ export const renderTable = ({
const columns = columnsFromArgs
? columnsFromArgs?.filter((column) =>
flattenTopLevelFields(clientCollectionConfig.fields, true)?.some(
(field) => 'name' in field && field.name in column,
(field) => 'name' in field && field.name === column.accessor,
),
)
: getInitialColumns(
@@ -196,23 +195,6 @@ export const renderTable = ({
} as Column)
}
const showDragHandle = docs.length > 0 && ORDER_FIELD_NAME in docs[0]
if (showDragHandle) {
columnsToUse.unshift({
accessor: '_dragHandle',
active: true,
field: {
admin: {
disabled: true,
},
hidden: true,
},
Heading: '', // Empty header
renderedCells: docs.map((_, i) => <SortRow key={i} rowData={docs[i]} />),
} as Column)
}
return {
columnState,
Table: <Table appearance={tableAppearance} columns={columnsToUse} data={docs} />,

View File

@@ -49,12 +49,6 @@
display: flex;
min-width: unset;
}
#heading-_dragHandle,
.cell-_dragHandle {
width: 20px;
min-width: 0;
}
}
}

View File

@@ -49,7 +49,9 @@ export function DefaultListView(props: ListViewClientProps) {
enableRowSelections,
hasCreatePermission: hasCreatePermissionFromProps,
listMenuItems,
listPreferences,
newDocumentURL,
preferenceKey,
renderedFilters,
resolvedFilterOptions,
Table: InitialTable,
@@ -149,10 +151,17 @@ export function DefaultListView(props: ListViewClientProps) {
])
}
}, [setStepNav, labels, drawerDepth])
return (
<Fragment>
<TableColumnsProvider collectionSlug={collectionSlug} columnState={columnState}>
<TableColumnsProvider
collectionSlug={collectionSlug}
columnState={columnState}
docs={docs}
enableRowSelections={enableRowSelections}
listPreferences={listPreferences}
preferenceKey={preferenceKey}
setTable={setTable}
>
<div className={`${baseClass} ${baseClass}--${collectionSlug}`}>
<SelectionProvider docs={docs} totalDocs={data.totalDocs} user={user}>
{BeforeList}

91
pnpm-lock.yaml generated
View File

@@ -45,7 +45,7 @@ importers:
version: 1.50.0
'@sentry/nextjs':
specifier: ^8.33.1
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
'@sentry/node':
specifier: ^8.33.1
version: 8.37.1
@@ -135,7 +135,7 @@ importers:
version: 10.1.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
next:
specifier: 15.1.5
version: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
version: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
open:
specifier: ^10.1.0
version: 10.1.0
@@ -810,9 +810,6 @@ importers:
file-type:
specifier: 19.3.0
version: 19.3.0
fractional-indexing:
specifier: 3.2.0
version: 3.2.0
get-tsconfig:
specifier: 4.8.1
version: 4.8.1
@@ -1014,7 +1011,7 @@ importers:
dependencies:
next:
specifier: ^15.0.3
version: 15.1.3(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
version: 15.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
devDependencies:
'@payloadcms/eslint-config':
specifier: workspace:*
@@ -1076,7 +1073,7 @@ importers:
dependencies:
'@sentry/nextjs':
specifier: ^8.33.1
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
'@sentry/types':
specifier: ^8.33.1
version: 8.37.1
@@ -1426,7 +1423,7 @@ importers:
version: link:../plugin-cloud-storage
uploadthing:
specifier: 7.3.0
version: 7.3.0(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))
version: 7.3.0(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))
devDependencies:
payload:
specifier: workspace:*
@@ -1706,7 +1703,7 @@ importers:
version: link:../packages/ui
'@sentry/nextjs':
specifier: ^8.33.1
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
'@sentry/react':
specifier: ^7.77.0
version: 7.119.2(react@19.0.0)
@@ -1760,7 +1757,7 @@ importers:
version: 8.9.5(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
next:
specifier: 15.1.5
version: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
version: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
nodemailer:
specifier: 6.9.16
version: 6.9.16
@@ -3658,67 +3655,79 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@@ -4034,42 +4043,49 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-arm64-musl@1.0.1':
resolution: {integrity: sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/nice-linux-ppc64-gnu@1.0.1':
resolution: {integrity: sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==}
engines: {node: '>= 10'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-riscv64-gnu@1.0.1':
resolution: {integrity: sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-s390x-gnu@1.0.1':
resolution: {integrity: sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==}
engines: {node: '>= 10'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-x64-gnu@1.0.1':
resolution: {integrity: sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-x64-musl@1.0.1':
resolution: {integrity: sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/nice-win32-arm64-msvc@1.0.1':
resolution: {integrity: sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==}
@@ -4158,72 +4174,84 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-gnu@15.1.3':
resolution: {integrity: sha512-YbdaYjyHa4fPK4GR4k2XgXV0p8vbU1SZh7vv6El4bl9N+ZSiMfbmqCuCuNU1Z4ebJMumafaz6UCC2zaJCsdzjw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-gnu@15.1.5':
resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.0.3':
resolution: {integrity: sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-arm64-musl@15.1.3':
resolution: {integrity: sha512-qgH/aRj2xcr4BouwKG3XdqNu33SDadqbkqB6KaZZkozar857upxKakbRllpqZgWl/NDeSCBYPmUAZPBHZpbA0w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-arm64-musl@15.1.5':
resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.0.3':
resolution: {integrity: sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-gnu@15.1.3':
resolution: {integrity: sha512-uzafnTFwZCPN499fNVnS2xFME8WLC9y7PLRs/yqz5lz1X/ySoxfaK2Hbz74zYUdEg+iDZPd8KlsWaw9HKkLEVw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-gnu@15.1.5':
resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.0.3':
resolution: {integrity: sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-musl@15.1.3':
resolution: {integrity: sha512-el6GUFi4SiDYnMTTlJJFMU+GHvw0UIFnffP1qhurrN1qJV3BqaSRUjkDUgVV44T6zpw1Lc6u+yn0puDKHs+Sbw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-musl@15.1.5':
resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.0.3':
resolution: {integrity: sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==}
@@ -4510,21 +4538,25 @@ packages:
resolution: {integrity: sha512-otVbS4zeo3n71zgGLBYRTriDzc0zpruC0WI3ICwjpIk454cLwGV0yzh4jlGYWQJYJk0BRAmXFd3ooKIF+bKBHw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-arm64-musl@1.12.0':
resolution: {integrity: sha512-IStQDjIT7Lzmqg1i9wXvPL/NsYsxF24WqaQFS8b8rxra+z0VG7saBOsEnOaa4jcEY8MVpLYabFhTV+fSsA2vnA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-linux-x64-gnu@1.12.0':
resolution: {integrity: sha512-SipT7EVORz8pOQSFwemOm91TpSiBAGmOjG830/o+aLEsvQ4pEy223+SAnCfITh7+AahldYsJnVoIs519jmIlKQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-musl@1.12.0':
resolution: {integrity: sha512-mGh0XfUzKdn+WFaqPacziNraCWL5znkHRfQVxG9avGS9zb2KC/N1EBbPzFqutDwixGDP54r2gx4q54YCJEZ4iQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-wasm32-wasi@1.12.0':
resolution: {integrity: sha512-SZN6v7apKmQf/Vwiqb6e/s3Y2Oacw8uW8V2i1AlxtyaEFvnFE0UBn89zq6swEwE3OCajNWs0yPvgAXUMddYc7Q==}
@@ -5012,24 +5044,28 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@swc/core-linux-arm64-musl@1.10.12':
resolution: {integrity: sha512-oqhSmV+XauSf0C//MoQnVErNUB/5OzmSiUzuazyLsD5pwqKNN+leC3JtRQ/QVzaCpr65jv9bKexT9+I2Tt3xDw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@swc/core-linux-x64-gnu@1.10.12':
resolution: {integrity: sha512-XldSIHyjD7m1Gh+/8rxV3Ok711ENLI420CU2EGEqSe3VSGZ7pHJvJn9ZFbYpWhsLxPqBYMFjp3Qw+J6OXCPXCA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@swc/core-linux-x64-musl@1.10.12':
resolution: {integrity: sha512-wvPXzJxzPgTqhyp1UskOx1hRTtdWxlyFD1cGWOxgLsMik0V9xKRgqKnMPv16Nk7L9xl6quQ6DuUHj9ID7L3oVw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@swc/core-win32-arm64-msvc@1.10.12':
resolution: {integrity: sha512-TUYzWuu1O7uyIcRfxdm6Wh1u+gNnrW5M1DUgDOGZLsyQzgc2Zjwfh2llLhuAIilvCVg5QiGbJlpibRYJ/8QGsg==}
@@ -5810,7 +5846,6 @@ packages:
bson@6.10.1:
resolution: {integrity: sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==}
engines: {node: '>=16.20.1'}
deprecated: a critical bug affecting only useBigInt64=true deserialization usage is fixed in bson@6.10.3
buffer-builder@0.2.0:
resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
@@ -6968,10 +7003,6 @@ packages:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
fractional-indexing@3.2.0:
resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==}
engines: {node: ^14.13.1 || >=16.0.0}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -7846,7 +7877,6 @@ packages:
libsql@0.4.7:
resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
lie@3.1.1:
@@ -13680,7 +13710,7 @@ snapshots:
'@sentry/utils': 7.119.2
localforage: 1.10.0
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))':
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
@@ -13694,9 +13724,9 @@ snapshots:
'@sentry/types': 8.37.1
'@sentry/utils': 8.37.1
'@sentry/vercel-edge': 8.37.1
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
chalk: 3.0.0
next: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
next: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
resolve: 1.22.8
rollup: 3.29.5
stacktrace-parser: 0.1.10
@@ -13804,12 +13834,12 @@ snapshots:
'@sentry/types': 8.37.1
'@sentry/utils': 8.37.1
'@sentry/webpack-plugin@2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))':
'@sentry/webpack-plugin@2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))':
dependencies:
'@sentry/bundler-plugin-core': 2.22.6
unplugin: 1.0.1
uuid: 9.0.0
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))
transitivePeerDependencies:
- encoding
- supports-color
@@ -16550,8 +16580,6 @@ snapshots:
dependencies:
fetch-blob: 3.2.0
fractional-indexing@3.2.0: {}
fs-constants@1.0.0: {}
fs-extra@10.1.0:
@@ -18190,7 +18218,7 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
next@15.1.3(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
next@15.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
dependencies:
'@next/env': 15.1.3
'@swc/counter': 0.1.3
@@ -18218,7 +18246,7 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
dependencies:
'@next/env': 15.1.5
'@swc/counter': 0.1.3
@@ -19619,17 +19647,16 @@ snapshots:
ansi-escapes: 4.3.2
supports-hyperlinks: 2.3.0
terser-webpack-plugin@5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)):
terser-webpack-plugin@5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.2
terser: 5.36.0
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))
optionalDependencies:
'@swc/core': 1.10.12(@swc/helpers@0.5.15)
esbuild: 0.19.12
terser@5.36.0:
dependencies:
@@ -19910,14 +19937,14 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
uploadthing@7.3.0(next@15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)):
uploadthing@7.3.0(next@15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)):
dependencies:
'@effect/platform': 0.69.8(effect@3.10.3)
'@uploadthing/mime-types': 0.3.2
'@uploadthing/shared': 7.1.1
effect: 3.10.3
optionalDependencies:
next: 15.1.5(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
next: 15.1.5(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
uri-js@4.4.1:
dependencies:
@@ -20015,7 +20042,7 @@ snapshots:
webpack-virtual-modules@0.5.0: {}
webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12):
webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.6
@@ -20037,7 +20064,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
terser-webpack-plugin: 5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
watchpack: 2.4.2
webpack-sources: 3.2.3
transitivePeerDependencies:

View File

@@ -1,38 +0,0 @@
'use client'
import { useField } from '@payloadcms/ui'
export function AfterField() {
const { setValue } = useField({ path: 'customJSON' })
return (
<button
id="set-custom-json"
onClick={(e) => {
e.preventDefault()
setValue({
users: [
{
id: 1,
name: 'John Doe',
email: 'john.doe@example.com',
isActive: true,
roles: ['admin', 'editor'],
},
{
id: 2,
name: 'Jane Smith',
email: 'jane.smith@example.com',
isActive: false,
roles: ['viewer'],
},
],
})
}}
style={{ marginTop: '5px', padding: '5px 10px' }}
type="button"
>
Set Custom JSON
</button>
)
}

View File

@@ -103,24 +103,4 @@ describe('JSON', () => {
'"foo.with.periods": "bar"',
)
})
test('should update', async () => {
const createdDoc = await payload.create({
collection: 'json-fields',
data: {
customJSON: {
default: 'value',
},
},
})
await page.goto(url.edit(createdDoc.id))
const jsonField = page.locator('.json-field #field-customJSON')
await expect(jsonField).toContainText('"default": "value"')
const originalHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
await page.locator('#set-custom-json').click()
const newHeight = (await page.locator('#field-customJSON').boundingBox())?.height || 0
expect(newHeight).toBeGreaterThan(originalHeight)
})
})

View File

@@ -67,16 +67,6 @@ const JSON: CollectionConfig = {
},
],
},
{
name: 'customJSON',
type: 'json',
admin: {
components: {
afterInput: ['./collections/JSON/AfterField#AfterField'],
},
},
label: 'Custom Json',
},
],
versions: {
maxPerDoc: 1,

View File

@@ -170,7 +170,12 @@ describe('Text', () => {
user: client.user,
key: 'text-fields-list',
value: {
columns: [{ disableListColumnText: true }],
columns: [
{
accessor: 'disableListColumnText',
active: true,
},
],
},
})

View File

@@ -1474,15 +1474,6 @@ export interface JsonField {
| boolean
| null;
};
customJSON?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
@@ -3174,7 +3165,6 @@ export interface JsonFieldsSelect<T extends boolean = true> {
| {
jsonWithinGroup?: T;
};
customJSON?: T;
updatedAt?: T;
createdAt?: T;
}

View File

@@ -329,9 +329,9 @@ describe('Localization', () => {
await page.goto(url.list)
const localeLabel = page.locator(
'.localizer.app-header__localizer .localizer-button__current-label',
)
const localeLabel = page
.locator('.localizer.app-header__localizer .localizer-button__current-label')
await expect(localeLabel).not.toHaveText('English')
})

View File

@@ -64,7 +64,6 @@ export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
richText: RichText;
'blocks-fields': BlocksField;

View File

@@ -1,19 +0,0 @@
/* eslint-disable no-console */
'use client'
export const Seed = () => {
return (
<button
onClick={async () => {
try {
await fetch('/api/seed', { method: 'POST' })
} catch (error) {
console.error(error)
}
}}
type="button"
>
Seed
</button>
)
}

View File

@@ -4,12 +4,8 @@ export const postsSlug = 'posts'
export const PostsCollection: CollectionConfig = {
slug: postsSlug,
enableCustomOrder: true,
admin: {
useAsTitle: 'text',
components: {
beforeList: ['/Seed#Seed'],
},
},
fields: [
{

View File

@@ -1,5 +1,3 @@
import type { CollectionSlug, Payload } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
@@ -19,30 +17,6 @@ export default buildConfigWithDefaults({
baseDir: path.resolve(dirname),
},
},
endpoints: [
{
path: '/seed',
method: 'post',
handler: async (req) => {
await req.payload.delete({ collection: 'posts', where: {} })
await createData(req.payload, 'posts', [
{ text: 'Post 1' },
{ text: 'Post 2' },
{ text: 'Post 3' },
{ text: 'Post 4' },
{ text: 'Post 5' },
{ text: 'Post 6' },
{ text: 'Post 7' },
{ text: 'Post 8' },
{ text: 'Post 9' },
])
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
status: 200,
})
},
},
],
cors: ['http://localhost:3000', 'http://localhost:3001'],
localization: {
locales: ['en', 'nb'],
@@ -56,33 +30,8 @@ export default buildConfigWithDefaults({
password: devUser.password,
},
})
await createData(payload, 'posts', [
{ text: 'Post 1', number: 1, number2: 10, group: { number: 100 } },
{ text: 'Post 2', number: 2, number2: 10, group: { number: 200 } },
{ text: 'Post 3', number: 3, number2: 5, group: { number: 150 } },
{ text: 'Post 10', number: 10, number2: 5, group: { number: 200 } },
{ text: 'Post 11', number: 11, number2: 20, group: { number: 150 } },
{ text: 'Post 12', number: 12, number2: 20, group: { number: 100 } },
])
await createData(payload, 'default-sort', [
{ text: 'Post default-5 b', number: 5 },
{ text: 'Post default-10', number: 10 },
{ text: 'Post default-5 a', number: 5 },
{ text: 'Post default-1', number: 1 },
])
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
export async function createData(
payload: Payload,
collection: CollectionSlug,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: Record<string, any>[],
) {
for (const item of data) {
await payload.create({ collection, data: item })
}
}

View File

@@ -6,10 +6,65 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
posts: Post;
drafts: Draft;
@@ -69,7 +124,6 @@ export interface UserAuthOperations {
*/
export interface Post {
id: string;
'payload-order'?: string | null;
text?: string | null;
number?: number | null;
number2?: number | null;
@@ -211,7 +265,6 @@ export interface PayloadMigration {
* via the `definition` "posts_select".
*/
export interface PostsSelect<T extends boolean = true> {
'payload-order'?: T;
text?: T;
number?: T;
number2?: T;

View File

@@ -868,7 +868,7 @@ describe('Versions', () => {
const publishOptions = page.locator('.doc-controls__controls .popup')
await publishOptions.click()
const publishSpecificLocale = page.locator('#publish-locale')
const publishSpecificLocale = page.locator('.popup-button-list button').first()
await expect(publishSpecificLocale).toContainText('English')
await publishSpecificLocale.click()

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/admin/config.ts"],
"@payload-config": ["./test/fields-relationship/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],