From f7172b5b2c0342d2335833090699b09bb3327fdf Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Tue, 10 Dec 2024 15:11:44 -0500 Subject: [PATCH] fix(ui): refreshes column state during hmr and respects admin.disableListColumn despite preferences (#9846) Partial fix for #9774. When `admin.disableListColumn` is set retroactively, it continues to appear in column state, but shouldn't. This was because the table column context was not refreshing after HMR runs, and would instead hold onto these stale columns until the page itself refreshes. Similarly, this was also a problem when the user had saved any of these columns to their list preferences, where those prefs would take precedence despite these properties being set on the underlying fields. The fix is to filter these columns from all requests that send them, and ensure local component state properly refreshes itself. --- packages/next/src/views/List/index.tsx | 4 +- packages/payload/src/exports/shared.ts | 2 + .../src/utilities/flattenTopLevelFields.ts | 18 +++--- .../ListControls/getTextFieldsToBeSearched.ts | 7 +-- .../TableColumns/buildColumnState.tsx | 25 +++++--- .../elements/TableColumns/filterFields.tsx | 19 ++++-- .../TableColumns/getInitialColumns.ts | 11 +++- .../ui/src/elements/TableColumns/index.tsx | 9 ++- packages/ui/src/hooks/useUseAsTitle.ts | 5 +- packages/ui/src/utilities/buildTableState.ts | 8 +-- packages/ui/src/utilities/flattenFieldMap.ts | 38 ------------ packages/ui/src/utilities/renderTable.tsx | 46 +++++++++------ test/admin/e2e/2/e2e.spec.ts | 16 ++--- test/fields/collections/Text/e2e.spec.ts | 28 +++++++++ test/fields/collections/Text/index.ts | 2 - test/helpers/e2e/toggleColumn.ts | 1 - test/helpers/e2e/upsertPrefs.ts | 59 +++++++++++++++++++ test/helpers/rest.ts | 15 +++-- test/helpers/sdk/endpoint.ts | 1 + 19 files changed, 194 insertions(+), 120 deletions(-) delete mode 100644 packages/ui/src/utilities/flattenFieldMap.ts create mode 100644 test/helpers/e2e/upsertPrefs.ts diff --git a/packages/next/src/views/List/index.tsx b/packages/next/src/views/List/index.tsx index 8a389b33b4..4269246d10 100644 --- a/packages/next/src/views/List/index.tsx +++ b/packages/next/src/views/List/index.tsx @@ -171,13 +171,13 @@ export const renderListView = async ( const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug) const { columnState, Table } = renderTable({ - collectionConfig: clientCollectionConfig, + clientCollectionConfig, + collectionConfig, columnPreferences: listPreferences?.columns, customCellProps, docs: data.docs, drawerSlug, enableRowSelections, - fields, i18n: req.i18n, payload, useAsTitle, diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index 741e19cd9e..1e58ef6147 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -57,6 +57,8 @@ export { } from '../utilities/deepMerge.js' export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js' +export { flattenAllFields } from '../utilities/flattenAllFields.js' +export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js' export { getDataByPath } from '../utilities/getDataByPath.js' export { getSelectMode } from '../utilities/getSelectMode.js' diff --git a/packages/payload/src/utilities/flattenTopLevelFields.ts b/packages/payload/src/utilities/flattenTopLevelFields.ts index 5537c54eff..5427bfbac6 100644 --- a/packages/payload/src/utilities/flattenTopLevelFields.ts +++ b/packages/payload/src/utilities/flattenTopLevelFields.ts @@ -33,18 +33,14 @@ function flattenFields( fields: TField[], keepPresentationalFields?: boolean, ): FlattenedField[] { - return fields.reduce[]>((fieldsToUse, field) => { + return fields.reduce[]>((acc, field) => { if (fieldAffectsData(field) || (keepPresentationalFields && fieldIsPresentationalOnly(field))) { - return [...fieldsToUse, field as FlattenedField] - } - - if (fieldHasSubFields(field)) { - return [...fieldsToUse, ...flattenFields(field.fields as TField[], keepPresentationalFields)] - } - - if (field.type === 'tabs' && 'tabs' in field) { + acc.push(field as FlattenedField) + } else if (fieldHasSubFields(field)) { + acc.push(...flattenFields(field.fields as TField[], keepPresentationalFields)) + } else if (field.type === 'tabs' && 'tabs' in field) { return [ - ...fieldsToUse, + ...acc, ...field.tabs.reduce[]>((tabFields, tab: TabType) => { if (tabHasName(tab)) { return [...tabFields, { ...tab, type: 'tab' } as unknown as FlattenedField] @@ -58,7 +54,7 @@ function flattenFields( ] } - return fieldsToUse + return acc }, []) } diff --git a/packages/ui/src/elements/ListControls/getTextFieldsToBeSearched.ts b/packages/ui/src/elements/ListControls/getTextFieldsToBeSearched.ts index e76f555ece..f24d0e1bde 100644 --- a/packages/ui/src/elements/ListControls/getTextFieldsToBeSearched.ts +++ b/packages/ui/src/elements/ListControls/getTextFieldsToBeSearched.ts @@ -1,16 +1,15 @@ 'use client' import type { ClientField } from 'payload' -import { fieldAffectsData } from 'payload/shared' - -import { flattenFieldMap } from '../../utilities/flattenFieldMap.js' +import { fieldAffectsData, flattenTopLevelFields } from 'payload/shared' export const getTextFieldsToBeSearched = ( listSearchableFields: string[], fields: ClientField[], ): ClientField[] => { if (listSearchableFields) { - const flattenedFields = flattenFieldMap(fields) + const flattenedFields = flattenTopLevelFields(fields) as ClientField[] + return flattenedFields.filter( (field) => fieldAffectsData(field) && listSearchableFields.includes(field.name), ) diff --git a/packages/ui/src/elements/TableColumns/buildColumnState.tsx b/packages/ui/src/elements/TableColumns/buildColumnState.tsx index 0d047f1e94..73afc0c9c5 100644 --- a/packages/ui/src/elements/TableColumns/buildColumnState.tsx +++ b/packages/ui/src/elements/TableColumns/buildColumnState.tsx @@ -1,6 +1,7 @@ import type { I18nClient } from '@payloadcms/translations' import type { ClientCollectionConfig, + ClientField, DefaultCellComponentProps, DefaultServerCellComponentProps, Field, @@ -16,6 +17,7 @@ import { fieldIsHiddenOrDisabled, fieldIsID, fieldIsPresentationalOnly, + flattenTopLevelFields, } from 'payload/shared' import React from 'react' @@ -31,19 +33,19 @@ import { SortColumn, // eslint-disable-next-line payload/no-imports-from-exports-dir } from '../../exports/client/index.js' -import { flattenFieldMap } from '../../utilities/flattenFieldMap.js' import { RenderServerComponent } from '../RenderServerComponent/index.js' +import { filterFields } from './filterFields.js' type Args = { beforeRows?: Column[] - collectionConfig: ClientCollectionConfig + clientCollectionConfig: ClientCollectionConfig + collectionConfig: SanitizedCollectionConfig columnPreferences: ColumnPreferences columns?: ColumnPreferences customCellProps: DefaultCellComponentProps['customCellProps'] docs: PaginatedDocs['docs'] enableRowSelections: boolean enableRowTypes?: boolean - fields: Field[] i18n: I18nClient payload: Payload sortColumnProps?: Partial @@ -53,24 +55,29 @@ type Args = { export const buildColumnState = (args: Args): Column[] => { const { beforeRows, + clientCollectionConfig, collectionConfig, columnPreferences, columns, customCellProps, docs, enableRowSelections, - fields, i18n, payload, sortColumnProps, useAsTitle, } = args - const clientFields = collectionConfig.fields - // clientFields contains the fake `id` column - let sortedFieldMap = flattenFieldMap(clientFields) - let _sortedFieldMap = flattenFieldMap(fields) // TODO: think of a way to avoid this additional flatten + let sortedFieldMap = flattenTopLevelFields( + filterFields(clientCollectionConfig.fields), + true, + ) as ClientField[] + + let _sortedFieldMap = flattenTopLevelFields( + filterFields(collectionConfig.fields), + true, + ) as Field[] // TODO: think of a way to avoid this additional flatten // place the `ID` field first, if it exists // do the same for the `useAsTitle` field with precedence over the `ID` field @@ -180,7 +187,7 @@ export const buildColumnState = (args: Args): Column[] => { const baseCellClientProps: DefaultCellComponentProps = { cellData: undefined, - collectionConfig: deepCopyObjectSimple(collectionConfig), + collectionConfig: deepCopyObjectSimple(clientCollectionConfig), customCellProps, field, rowData: undefined, diff --git a/packages/ui/src/elements/TableColumns/filterFields.tsx b/packages/ui/src/elements/TableColumns/filterFields.tsx index 82ce920ee9..d45e0e2b8a 100644 --- a/packages/ui/src/elements/TableColumns/filterFields.tsx +++ b/packages/ui/src/elements/TableColumns/filterFields.tsx @@ -1,17 +1,22 @@ import type { ClientField, Field } from 'payload' -// 1. Skips fields that are hidden, disabled, or presentational-only (i.e. `ui` fields) -// 2. Maps through top-level `tabs` fields and filters out the same +import { fieldIsHiddenOrDisabled, fieldIsID } from 'payload/shared' + +/** + * Filters fields that are hidden, disabled, or have `disableListColumn` set to `true` + * Does so recursively for `tabs` fields. + */ export const filterFields = (incomingFields: T[]): T[] => { const shouldSkipField = (field: T): boolean => - (field.type !== 'ui' && field.admin?.disabled === true) || + (field.type !== 'ui' && fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) || field?.admin?.disableListColumn === true - const fields: T[] = incomingFields?.reduce((formatted, field) => { + const fields: T[] = incomingFields?.reduce((acc, field) => { if (shouldSkipField(field)) { - return formatted + return acc } + // extract top-level `tabs` fields and filter out the same const formattedField: T = field.type === 'tabs' && 'tabs' in field ? { @@ -23,7 +28,9 @@ export const filterFields = (incomingFields: T[]) } : field - return [...formatted, formattedField] + acc.push(formattedField) + + return acc }, []) return fields diff --git a/packages/ui/src/elements/TableColumns/getInitialColumns.ts b/packages/ui/src/elements/TableColumns/getInitialColumns.ts index 9fcb1d3f3c..e52bc2bb03 100644 --- a/packages/ui/src/elements/TableColumns/getInitialColumns.ts +++ b/packages/ui/src/elements/TableColumns/getInitialColumns.ts @@ -1,4 +1,4 @@ -import type { ClientField, Field } from 'payload' +import type { ClientField, CollectionConfig, Field } from 'payload' import { fieldAffectsData } from 'payload/shared' @@ -33,10 +33,15 @@ const getRemainingColumns = ( return [...remaining, field.name] }, []) +/** + * Returns the initial columns to display in the table based on the following criteria: + * 1. If `defaultColumns` is set in the collection config, use those columns + * 2. Otherwise take `useAtTitle, if set, and the next 3 fields that are not hidden or disabled + */ export const getInitialColumns = ( fields: T, - useAsTitle: string, - defaultColumns: string[], + useAsTitle: CollectionConfig['admin']['useAsTitle'], + defaultColumns: CollectionConfig['admin']['defaultColumns'], ): ColumnPreferences => { let initialColumns = [] diff --git a/packages/ui/src/elements/TableColumns/index.tsx b/packages/ui/src/elements/TableColumns/index.tsx index 3d3251b740..52852d145b 100644 --- a/packages/ui/src/elements/TableColumns/index.tsx +++ b/packages/ui/src/elements/TableColumns/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { ClientCollectionConfig, SanitizedCollectionConfig } from 'payload' -import React, { createContext, useCallback, useContext } from 'react' +import React, { createContext, useCallback, useContext, useEffect } from 'react' import type { ColumnPreferences } from '../../providers/ListQuery/index.js' import type { SortColumnProps } from '../SortColumn/index.js' @@ -204,6 +204,7 @@ export const TableColumnsProvider: React.FC = ({ return indexOfFirst > indexOfSecond ? 1 : -1 }) + const { state: columnState, Table } = await getTableState({ collectionSlug, columns: activeColumns, @@ -275,7 +276,11 @@ export const TableColumnsProvider: React.FC = ({ sortColumnProps, ]) - React.useEffect(() => { + useEffect(() => { + setTableColumns(columnState) + }, [columnState]) + + useEffect(() => { return () => { abortAndIgnore(tableStateControllerRef.current) } diff --git a/packages/ui/src/hooks/useUseAsTitle.ts b/packages/ui/src/hooks/useUseAsTitle.ts index 40272e7a04..0449e7eadb 100644 --- a/packages/ui/src/hooks/useUseAsTitle.ts +++ b/packages/ui/src/hooks/useUseAsTitle.ts @@ -1,7 +1,7 @@ 'use client' import type { ClientCollectionConfig, ClientField } from 'payload' -import { flattenFieldMap } from '../utilities/flattenFieldMap.js' +import { flattenTopLevelFields } from 'payload/shared' export const useUseTitleField = (collection: ClientCollectionConfig): ClientField => { const { @@ -9,6 +9,7 @@ export const useUseTitleField = (collection: ClientCollectionConfig): ClientFiel fields, } = collection - const topLevelFields = flattenFieldMap(fields) + const topLevelFields = flattenTopLevelFields(fields) as ClientField[] + return topLevelFields?.find((field) => 'name' in field && field.name === useAsTitle) } diff --git a/packages/ui/src/utilities/buildTableState.ts b/packages/ui/src/utilities/buildTableState.ts index 3a78c8cb87..1c041cc822 100644 --- a/packages/ui/src/utilities/buildTableState.ts +++ b/packages/ui/src/utilities/buildTableState.ts @@ -198,8 +198,6 @@ export const buildTableState = async ( } } - const fields = collectionConfig.fields - let docs = docsFromArgs let data: PaginatedDocs @@ -219,12 +217,12 @@ export const buildTableState = async ( } const { columnState, Table } = renderTable({ - collectionConfig: clientCollectionConfig, + clientCollectionConfig, + collectionConfig, columnPreferences: undefined, // TODO, might not be needed columns, docs, enableRowSelections, - fields, i18n: req.i18n, payload, renderRowTypes, @@ -232,7 +230,7 @@ export const buildTableState = async ( useAsTitle: collectionConfig.admin.useAsTitle, }) - const renderedFilters = renderFilters(fields, req.payload.importMap) + const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap) return { data, diff --git a/packages/ui/src/utilities/flattenFieldMap.ts b/packages/ui/src/utilities/flattenFieldMap.ts deleted file mode 100644 index 64b7500043..0000000000 --- a/packages/ui/src/utilities/flattenFieldMap.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ClientField, Field } from 'payload' - -import { fieldIsPresentationalOnly } from 'payload/shared' - -/** - * Flattens a collection's fields into a single array of fields, as long - * as the fields do not affect data. - * - * @param fields - * @param keepPresentationalFields if true, will skip flattening fields that are presentational only - */ -export const flattenFieldMap = ( - fields: T[], - keepPresentationalFields?: boolean, -): T[] => { - return fields?.reduce((acc, field) => { - if ('name' in field || (keepPresentationalFields && fieldIsPresentationalOnly(field))) { - acc.push(field) - return acc - } else if ('fields' in field) { - acc.push(...flattenFieldMap(field.fields as T[], keepPresentationalFields)) - } else if (field.type === 'tabs' && 'tabs' in field && Array.isArray(field.tabs)) { - return [ - ...acc, - ...field.tabs.reduce((tabAcc, tab) => { - return [ - ...tabAcc, - ...('name' in tab - ? [{ ...tab }] - : flattenFieldMap(tab.fields as T[], keepPresentationalFields)), - ] - }, []), - ] - } - - return acc - }, []) -} diff --git a/packages/ui/src/utilities/renderTable.tsx b/packages/ui/src/utilities/renderTable.tsx index 99ccd34835..ce1f0a5662 100644 --- a/packages/ui/src/utilities/renderTable.tsx +++ b/packages/ui/src/utilities/renderTable.tsx @@ -1,14 +1,14 @@ -import type { - ClientCollectionConfig, - CollectionConfig, - Field, - ImportMap, - PaginatedDocs, - Payload, -} from 'payload' - import { getTranslation, type I18nClient } from '@payloadcms/translations' -import { fieldIsHiddenOrDisabled, fieldIsID } from 'payload/shared' +import { + type ClientCollectionConfig, + type CollectionConfig, + type Field, + type ImportMap, + type PaginatedDocs, + type Payload, + type SanitizedCollectionConfig, +} from 'payload' +import { fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared' // eslint-disable-next-line payload/no-imports-from-exports-dir import type { Column } from '../exports/client/index.js' @@ -18,7 +18,6 @@ import { RenderServerComponent } from '../elements/RenderServerComponent/index.j import { buildColumnState } from '../elements/TableColumns/buildColumnState.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, Table } from '../exports/client/index.js' @@ -48,27 +47,27 @@ export const renderFilters = ( ) export const renderTable = ({ + clientCollectionConfig, collectionConfig, columnPreferences, columns: columnsFromArgs, customCellProps, docs, enableRowSelections, - fields, i18n, payload, renderRowTypes, tableAppearance, useAsTitle, }: { - collectionConfig: ClientCollectionConfig + clientCollectionConfig: ClientCollectionConfig + collectionConfig: SanitizedCollectionConfig columnPreferences: ColumnPreferences columns?: ColumnPreferences customCellProps?: Record docs: PaginatedDocs['docs'] drawerSlug?: string enableRowSelections: boolean - fields: Field[] i18n: I18nClient payload: Payload renderRowTypes?: boolean @@ -78,9 +77,18 @@ export const renderTable = ({ columnState: Column[] Table: React.ReactNode } => { - const columns = - columnsFromArgs || - getInitialColumns(filterFields(fields), useAsTitle, collectionConfig?.admin?.defaultColumns) + // Ensure that columns passed as args comply with the field config, i.e. `hidden`, `disableListColumn`, etc. + const columns = columnsFromArgs + ? columnsFromArgs?.filter((column) => + flattenTopLevelFields(clientCollectionConfig.fields, true)?.some( + (field) => 'name' in field && field.name === column.accessor, + ), + ) + : getInitialColumns( + filterFields(clientCollectionConfig.fields), + useAsTitle, + clientCollectionConfig?.admin?.defaultColumns, + ) const columnState = buildColumnState({ beforeRows: renderRowTypes @@ -91,16 +99,16 @@ export const renderTable = ({ field: null, Heading: i18n.t('version:type'), renderedCells: docs.map((_, i) => ( - {getTranslation(collectionConfig.labels.singular, i18n)} + {getTranslation(clientCollectionConfig.labels.singular, i18n)} )), }, ] : undefined, + clientCollectionConfig, collectionConfig, columnPreferences, columns, enableRowSelections, - fields, i18n, // sortColumnProps, customCellProps, diff --git a/test/admin/e2e/2/e2e.spec.ts b/test/admin/e2e/2/e2e.spec.ts index 86d2c1beca..bdd2394cec 100644 --- a/test/admin/e2e/2/e2e.spec.ts +++ b/test/admin/e2e/2/e2e.spec.ts @@ -27,7 +27,7 @@ const description = 'Description' let payload: PayloadTestSDK import { goToFirstCell, navigateToDoc } from 'helpers/e2e/navigateToDoc.js' -import { toggleColumn } from 'helpers/e2e/toggleColumn.js' +import { openListColumns, toggleColumn } from 'helpers/e2e/toggleColumn.js' import path from 'path' import { wait } from 'payload/shared' import { fileURLToPath } from 'url' @@ -206,21 +206,16 @@ describe('admin2', () => { test('should toggle columns', async () => { const columnCountLocator = 'table > thead > tr > th' await createPost() - await page.locator('.list-controls__toggle-columns').click() + await openListColumns(page, {}) const numberOfColumns = await page.locator(columnCountLocator).count() await expect(page.locator('.column-selector')).toBeVisible() await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID') - - const idButton = page.locator(`.column-selector .column-selector__column`, { - hasText: exactText('ID'), - }) - - await idButton.click() + await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' }) await page.locator('#heading-id').waitFor({ state: 'detached' }) await page.locator('.cell-id').first().waitFor({ state: 'detached' }) await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1) await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('Number') - await idButton.click() + await toggleColumn(page, { columnLabel: 'ID', targetState: 'on' }) await expect(page.locator('.cell-id').first()).toBeVisible() await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns) await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID') @@ -744,8 +739,7 @@ describe('admin2', () => { test('should sort with existing filters', async () => { await page.goto(postsUrl.list) - const column = await toggleColumn(page, { columnLabel: 'ID' }) - await expect(column).not.toHaveClass('column-selector__column--active') + await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' }) await page.locator('#heading-id').waitFor({ state: 'detached' }) await page.locator('#heading-title button.sort-column__asc').click() await page.waitForURL(/sort=title/) diff --git a/test/fields/collections/Text/e2e.spec.ts b/test/fields/collections/Text/e2e.spec.ts index c9ae46c049..04d21c2416 100644 --- a/test/fields/collections/Text/e2e.spec.ts +++ b/test/fields/collections/Text/e2e.spec.ts @@ -1,7 +1,9 @@ import type { Page } from '@playwright/test' +import type { GeneratedTypes } from 'helpers/sdk/types.js' import { expect, test } from '@playwright/test' import { openListColumns, toggleColumn } from 'helpers/e2e/toggleColumn.js' +import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js' import path from 'path' import { wait } from 'payload/shared' import { fileURLToPath } from 'url' @@ -203,6 +205,32 @@ describe('Text', () => { ).toBeVisible() }) + test('should respect admin.disableListColumn despite preferences', async () => { + await upsertPrefs>({ + payload, + user: client.user, + value: { + columns: [ + { + accessor: 'disableListColumnText', + active: true, + }, + ], + }, + }) + + await page.goto(url.list) + await openListColumns(page, {}) + await expect( + page.locator(`.column-selector .column-selector__column`, { + hasText: exactText('Disable List Column Text'), + }), + ).toBeHidden() + + await expect(page.locator('#heading-disableListColumnText')).toBeHidden() + await expect(page.locator('table .row-1 .cell-disableListColumnText')).toBeHidden() + }) + test('should hide field in filter when admin.disableListFilter is true', async () => { await page.goto(url.list) await page.locator('.list-controls__toggle-where').click() diff --git a/test/fields/collections/Text/index.ts b/test/fields/collections/Text/index.ts index e718fd9f74..58cfa25271 100644 --- a/test/fields/collections/Text/index.ts +++ b/test/fields/collections/Text/index.ts @@ -156,14 +156,12 @@ const TextFields: CollectionConfig = { type: 'text', admin: { disableListColumn: true, - disableListFilter: false, }, }, { name: 'disableListFilterText', type: 'text', admin: { - disableListColumn: false, disableListFilter: true, }, }, diff --git a/test/helpers/e2e/toggleColumn.ts b/test/helpers/e2e/toggleColumn.ts index 0be1faf08e..79b1447110 100644 --- a/test/helpers/e2e/toggleColumn.ts +++ b/test/helpers/e2e/toggleColumn.ts @@ -1,7 +1,6 @@ import type { Page } from '@playwright/test' import { expect } from '@playwright/test' -import { wait } from 'payload/shared' import { exactText } from '../../helpers.js' diff --git a/test/helpers/e2e/upsertPrefs.ts b/test/helpers/e2e/upsertPrefs.ts new file mode 100644 index 0000000000..dd92add2af --- /dev/null +++ b/test/helpers/e2e/upsertPrefs.ts @@ -0,0 +1,59 @@ +import type { PayloadTestSDK } from 'helpers/sdk/index.js' +import type { GeneratedTypes } from 'helpers/sdk/types.js' +import type { TypedUser } from 'payload' + +export const upsertPrefs = async < + TConfig extends GeneratedTypes, + TGeneratedTypes extends GeneratedTypes, +>({ + payload, + user, + value, +}: { + payload: PayloadTestSDK + user: TypedUser + value: Record +}): Promise => { + let prefs = await payload + .find({ + collection: 'payload-preferences', + depth: 0, + limit: 1, + where: { + and: [ + { key: { equals: 'text-fields-list' } }, + { 'user.value': { equals: user.id } }, + { 'user.relationTo': { equals: user.collection } }, + ], + }, + }) + ?.then((res) => res.docs?.[0]) + + if (!prefs) { + prefs = await payload.create({ + collection: 'payload-preferences', + depth: 0, + data: { + key: 'text-fields-list', + user: { + relationTo: user.collection, + value: user.id, + }, + value, + }, + }) + } else { + prefs = await payload.update({ + collection: 'payload-preferences', + id: prefs.id, + data: { + value: { + ...(prefs?.value ?? {}), + ...value, + }, + }, + }) + } + + return prefs +} diff --git a/test/helpers/rest.ts b/test/helpers/rest.ts index 97e10b972e..02ed4937fc 100644 --- a/test/helpers/rest.ts +++ b/test/helpers/rest.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Where } from 'payload' -import type { Config } from 'payload' -import type { PaginatedDocs } from 'payload' +import type { Config, PaginatedDocs, TypedUser, Where } from 'payload' import * as qs from 'qs-esm' @@ -120,6 +118,8 @@ export class RESTClient { serverURL: string + public user: TypedUser + constructor(config: Config, args: Args) { this.config = config this.serverURL = args.serverURL @@ -254,7 +254,9 @@ export class RESTClient { const response = await fetch(`${this.serverURL}/api/${slug}${whereQuery}`, options) const { status } = response const result = await response.json() - if (result.errors) throw new Error(result.errors[0].message) + if (result.errors) { + throw new Error(result.errors[0].message) + } return { result, status } } @@ -310,7 +312,9 @@ export class RESTClient { method: 'POST', }) - let { token } = await response.json() + const { user } = await response.json() + + let token = user.token // If the token is not in the response body, then we can extract it from the cookies if (!token) { @@ -319,6 +323,7 @@ export class RESTClient { token = tokenMatchResult?.groups?.token } + this.user = user this.token = token return token diff --git a/test/helpers/sdk/endpoint.ts b/test/helpers/sdk/endpoint.ts index 42a7a90ffd..611eb651aa 100644 --- a/test/helpers/sdk/endpoint.ts +++ b/test/helpers/sdk/endpoint.ts @@ -7,6 +7,7 @@ export const handler: PayloadHandler = async (req) => { await addDataAndFileToRequest(req) const { data, payload, user } = req + const operation = data?.operation ? String(data.operation) : undefined if (data?.operation && typeof payload[operation] === 'function') {