diff --git a/next-env.d.ts b/next-env.d.ts index 830fb594ca..1b3be0840f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/ui/src/forms/Form/fieldReducer.ts b/packages/ui/src/forms/Form/fieldReducer.ts index f2aaef5732..1a51efc079 100644 --- a/packages/ui/src/forms/Form/fieldReducer.ts +++ b/packages/ui/src/forms/Form/fieldReducer.ts @@ -134,35 +134,37 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { case 'DUPLICATE_ROW': { const { path, rowIndex } = action const { remainingFields, rows } = separateRows(path, state) - const rowsMetadata = [...(state[path].rows || [])] + const rowsWithDuplicate = [...(state[path].rows || [])] - const duplicateRowMetadata = deepCopyObjectSimpleWithoutReactComponents( - rowsMetadata[rowIndex], - ) + const newRow = deepCopyObjectSimpleWithoutReactComponents(rowsWithDuplicate[rowIndex]) - if (duplicateRowMetadata.id) { - duplicateRowMetadata.id = new ObjectId().toHexString() + const newRowID = new ObjectId().toHexString() + + if (newRow.id) { + newRow.id = newRowID } - if (rowsMetadata[rowIndex]?.customComponents?.RowLabel) { - duplicateRowMetadata.customComponents = { - RowLabel: rowsMetadata[rowIndex].customComponents.RowLabel, + if (rowsWithDuplicate[rowIndex]?.customComponents?.RowLabel) { + newRow.customComponents = { + RowLabel: rowsWithDuplicate[rowIndex].customComponents.RowLabel, } } const duplicateRowState = deepCopyObjectSimpleWithoutReactComponents(rows[rowIndex]) if (duplicateRowState.id) { - duplicateRowState.id.value = new ObjectId().toHexString() - duplicateRowState.id.initialValue = new ObjectId().toHexString() + duplicateRowState.id.value = newRowID + duplicateRowState.id.initialValue = newRowID } for (const key of Object.keys(duplicateRowState).filter((key) => key.endsWith('.id'))) { const idState = duplicateRowState[key] + const newNestedFieldID = new ObjectId().toHexString() + if (idState && typeof idState.value === 'string' && ObjectId.isValid(idState.value)) { - duplicateRowState[key].value = new ObjectId().toHexString() - duplicateRowState[key].initialValue = new ObjectId().toHexString() + duplicateRowState[key].value = newNestedFieldID + duplicateRowState[key].initialValue = newNestedFieldID } } @@ -170,7 +172,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { if (Object.keys(duplicateRowState).length > 0) { // Add new object containing subfield names to unflattenedRows array rows.splice(rowIndex + 1, 0, duplicateRowState) - rowsMetadata.splice(rowIndex + 1, 0, duplicateRowMetadata) + rowsWithDuplicate.splice(rowIndex + 1, 0, newRow) } const newState = { @@ -179,7 +181,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState { [path]: { ...state[path], disableFormData: true, - rows: rowsMetadata, + rows: rowsWithDuplicate, value: rows.length, }, } diff --git a/test/_community/collections/Posts/index.ts b/test/_community/collections/Posts/index.ts index 86a307b39c..e49e89a924 100644 --- a/test/_community/collections/Posts/index.ts +++ b/test/_community/collections/Posts/index.ts @@ -1,7 +1,5 @@ import type { CollectionConfig } from 'payload' -import { lexicalEditor } from '@payloadcms/richtext-lexical' - export const postsSlug = 'posts' export const PostsCollection: CollectionConfig = { @@ -15,11 +13,14 @@ export const PostsCollection: CollectionConfig = { type: 'text', }, { - name: 'content', - type: 'richText', - editor: lexicalEditor({ - features: ({ defaultFeatures }) => [...defaultFeatures], - }), + name: 'array', + type: 'array', + fields: [ + { + name: 'title', + type: 'text', + }, + ], }, ], } diff --git a/test/_community/payload-types.ts b/test/_community/payload-types.ts index 599c9dec1d..ecebd076d1 100644 --- a/test/_community/payload-types.ts +++ b/test/_community/payload-types.ts @@ -126,21 +126,12 @@ export interface UserAuthOperations { export interface Post { id: string; title?: string | null; - content?: { - root: { - type: string; - children: { - type: string; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - } | null; + array?: + | { + title?: string | null; + id?: string | null; + }[] + | null; updatedAt: string; createdAt: string; } @@ -279,7 +270,12 @@ export interface PayloadMigration { */ export interface PostsSelect { title?: T; - content?: T; + array?: + | T + | { + title?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; } diff --git a/test/fields/collections/Blocks/e2e.spec.ts b/test/fields/collections/Blocks/e2e.spec.ts index 55f2d12625..ed3e040504 100644 --- a/test/fields/collections/Blocks/e2e.spec.ts +++ b/test/fields/collections/Blocks/e2e.spec.ts @@ -2,8 +2,14 @@ import type { BrowserContext, Page } from '@playwright/test' import { expect, test } from '@playwright/test' import { copyPasteField } from 'helpers/e2e/copyPasteField.js' -import { addArrayRowBelow, duplicateArrayRow } from 'helpers/e2e/fields/array/index.js' -import { addBlock, openBlocksDrawer, reorderBlocks } from 'helpers/e2e/fields/blocks/index.js' +import { duplicateArrayRow } from 'helpers/e2e/fields/array/index.js' +import { + addBlock, + addBlockBelow, + duplicateBlock, + openBlocksDrawer, + reorderBlocks, +} from 'helpers/e2e/fields/blocks/index.js' import { scrollEntirePage } from 'helpers/e2e/scrollEntirePage.js' import { toggleBlockOrArrayRow } from 'helpers/e2e/toggleCollapsible.js' import path from 'path' @@ -127,22 +133,13 @@ describe('Block fields', () => { test('should open blocks drawer from block row and add below', async () => { await page.goto(url.create) - await addArrayRowBelow(page, { fieldName: 'blocks' }) - - const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]') - await expect(blocksDrawer).toBeVisible() - - // select the first block in the drawer - const firstBlockSelector = blocksDrawer - .locator('.blocks-drawer__blocks .blocks-drawer__block') - .first() - - await expect(firstBlockSelector).toContainText('Content') - await firstBlockSelector.click() + await addBlockBelow(page, { fieldName: 'blocks', blockToSelect: 'Content' }) // ensure the block was inserted beneath the first in the rows const addedRow = page.locator('#field-blocks #blocks-row-1') + await expect(addedRow).toBeVisible() + await expect(addedRow.locator('.blocks-field__block-header')).toHaveText( 'Custom Block Label: Content 02', ) // went from `Number` to `Content` @@ -151,19 +148,17 @@ describe('Block fields', () => { test('should duplicate block', async () => { await page.goto(url.create) - await duplicateArrayRow(page, { fieldName: 'blocks' }) + const { rowCount } = await duplicateBlock(page, { fieldName: 'blocks' }) - const blocks = page.locator('#field-blocks > .blocks-field__rows > div') - expect(await blocks.count()).toEqual(5) + expect(rowCount).toEqual(5) }) test('should save when duplicating subblocks', async () => { await page.goto(url.create) - await duplicateArrayRow(page, { fieldName: 'blocks', rowIndex: 2 }) + const { rowCount } = await duplicateBlock(page, { fieldName: 'blocks', rowIndex: 2 }) - const blocks = page.locator('#field-blocks > .blocks-field__rows > div') - expect(await blocks.count()).toEqual(5) + expect(rowCount).toEqual(5) await page.click('#action-save') await expect(page.locator('.payload-toast-container')).toContainText('successfully') diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 9a2b3ff50e..43f1cff19e 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -149,7 +149,7 @@ export interface Config { 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; db: { - defaultIDType: number; + defaultIDType: string; }; globals: {}; globalsSelect: {}; @@ -215,7 +215,7 @@ export interface LocalizedTextReference2 { * via the `definition` "users". */ export interface User { - id: number; + id: string; canViewConditionalField?: boolean | null; updatedAt: string; createdAt: string; @@ -240,7 +240,7 @@ export interface User { * via the `definition` "select-versions-fields". */ export interface SelectVersionsField { - id: number; + id: string; hasMany?: ('a' | 'b' | 'c' | 'd')[] | null; array?: | { @@ -265,7 +265,7 @@ export interface SelectVersionsField { * via the `definition` "array-fields". */ export interface ArrayField { - id: number; + id: string; title?: string | null; items: { text: string; @@ -369,7 +369,7 @@ export interface ArrayField { * via the `definition` "block-fields". */ export interface BlockField { - id: number; + id: string; blocks: (ContentBlock | NoBlockname | NumberBlock | SubBlocksBlock | TabsBlock)[]; duplicate: (ContentBlock | NoBlockname | NumberBlock | SubBlocksBlock | TabsBlock)[]; collapsedByDefaultBlocks: ( @@ -500,7 +500,7 @@ export interface BlockField { | null; relationshipBlocks?: | { - relationship?: (number | null) | TextField; + relationship?: (string | null) | TextField; id?: string | null; blockName?: string | null; blockType: 'relationships'; @@ -697,7 +697,7 @@ export interface LocalizedTabsBlock { * via the `definition` "text-fields". */ export interface TextField { - id: number; + id: string; text: string; hiddenTextField?: string | null; /** @@ -749,7 +749,7 @@ export interface TextField { * via the `definition` "checkbox-fields". */ export interface CheckboxField { - id: number; + id: string; checkbox: boolean; checkboxNotRequired?: boolean | null; updatedAt: string; @@ -760,7 +760,7 @@ export interface CheckboxField { * via the `definition` "code-fields". */ export interface CodeField { - id: number; + id: string; javascript?: string | null; typescript?: string | null; json?: string | null; @@ -775,7 +775,7 @@ export interface CodeField { * via the `definition` "collapsible-fields". */ export interface CollapsibleField { - id: number; + id: string; text: string; group: { textWithinGroup?: string | null; @@ -808,7 +808,7 @@ export interface CollapsibleField { * via the `definition` "conditional-logic". */ export interface ConditionalLogic { - id: number; + id: string; text: string; toggleField?: boolean | null; fieldWithDocIDCondition?: string | null; @@ -922,7 +922,7 @@ export interface CustomRowId { * via the `definition` "date-fields". */ export interface DateField { - id: number; + id: string; default: string; timeOnly?: string | null; timeOnlyWithMiliseconds?: string | null; @@ -967,7 +967,7 @@ export interface DateField { * via the `definition` "email-fields". */ export interface EmailField { - id: number; + id: string; email: string; localizedEmail?: string | null; emailWithAutocomplete?: string | null; @@ -992,7 +992,7 @@ export interface EmailField { * via the `definition` "radio-fields". */ export interface RadioField { - id: number; + id: string; radio?: ('one' | 'two' | 'three') | null; radioWithJsxLabelOption?: ('one' | 'two' | 'three') | null; updatedAt: string; @@ -1003,7 +1003,7 @@ export interface RadioField { * via the `definition` "group-fields". */ export interface GroupField { - id: number; + id: string; /** * This is a group. */ @@ -1085,22 +1085,22 @@ export interface GroupField { select?: ('one' | 'two')[] | null; }; localizedGroupRel?: { - email?: (number | null) | EmailField; + email?: (string | null) | EmailField; }; localizedGroupManyRel?: { - email?: (number | EmailField)[] | null; + email?: (string | EmailField)[] | null; }; localizedGroupPolyRel?: { email?: { relationTo: 'email-fields'; - value: number | EmailField; + value: string | EmailField; } | null; }; localizedGroupPolyHasManyRel?: { email?: | { relationTo: 'email-fields'; - value: number | EmailField; + value: string | EmailField; }[] | null; }; @@ -1154,30 +1154,30 @@ export interface RowField { * via the `definition` "indexed-fields". */ export interface IndexedField { - id: number; + id: string; text: string; uniqueText?: string | null; - uniqueRelationship?: (number | null) | TextField; - uniqueHasManyRelationship?: (number | TextField)[] | null; - uniqueHasManyRelationship_2?: (number | TextField)[] | null; + uniqueRelationship?: (string | null) | TextField; + uniqueHasManyRelationship?: (string | TextField)[] | null; + uniqueHasManyRelationship_2?: (string | TextField)[] | null; uniquePolymorphicRelationship?: { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | null; uniquePolymorphicRelationship_2?: { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | null; uniqueHasManyPolymorphicRelationship?: | { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; }[] | null; uniqueHasManyPolymorphicRelationship_2?: | { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; }[] | null; uniqueRequiredText: string; @@ -1213,7 +1213,7 @@ export interface IndexedField { * via the `definition` "json-fields". */ export interface JsonField { - id: number; + id: string; json?: { array?: { object?: { @@ -1254,7 +1254,7 @@ export interface JsonField { * via the `definition` "number-fields". */ export interface NumberField { - id: number; + id: string; number?: number | null; min?: number | null; max?: number | null; @@ -1289,7 +1289,7 @@ export interface NumberField { * via the `definition` "point-fields". */ export interface PointField { - id: number; + id: string; /** * @minItems 2 * @maxItems 2 @@ -1320,83 +1320,83 @@ export interface PointField { * via the `definition` "relationship-fields". */ export interface RelationshipField { - id: number; + id: string; text?: string | null; relationship: | { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | { relationTo: 'array-fields'; - value: number | ArrayField; + value: string | ArrayField; }; relationHasManyPolymorphic?: | ( | { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | { relationTo: 'array-fields'; - value: number | ArrayField; + value: string | ArrayField; } )[] | null; - relationToSelf?: (number | null) | RelationshipField; - relationToSelfSelectOnly?: (number | null) | RelationshipField; - relationWithAllowCreateToFalse?: (number | null) | User; - relationWithAllowEditToFalse?: (number | null) | User; - relationWithDynamicDefault?: (number | null) | User; + relationToSelf?: (string | null) | RelationshipField; + relationToSelfSelectOnly?: (string | null) | RelationshipField; + relationWithAllowCreateToFalse?: (string | null) | User; + relationWithAllowEditToFalse?: (string | null) | User; + relationWithDynamicDefault?: (string | null) | User; relationHasManyWithDynamicDefault?: { relationTo: 'users'; - value: number | User; + value: string | User; } | null; - relationshipWithMin?: (number | TextField)[] | null; - relationshipWithMax?: (number | TextField)[] | null; - relationshipHasMany?: (number | TextField)[] | null; + relationshipWithMin?: (string | TextField)[] | null; + relationshipWithMax?: (string | TextField)[] | null; + relationshipHasMany?: (string | TextField)[] | null; array?: | { - relationship?: (number | null) | TextField; + relationship?: (string | null) | TextField; id?: string | null; }[] | null; relationshipWithMinRows?: | { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; }[] | null; relationToRow?: (string | null) | RowField; relationToRowMany?: (string | RowField)[] | null; - relationshipDrawer?: (number | null) | TextField; - relationshipDrawerReadOnly?: (number | null) | TextField; + relationshipDrawer?: (string | null) | TextField; + relationshipDrawerReadOnly?: (string | null) | TextField; polymorphicRelationshipDrawer?: | ({ relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | null) | ({ relationTo: 'array-fields'; - value: number | ArrayField; + value: string | ArrayField; } | null); - relationshipDrawerHasMany?: (number | TextField)[] | null; + relationshipDrawerHasMany?: (string | TextField)[] | null; relationshipDrawerHasManyPolymorphic?: | ( | { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | { relationTo: 'array-fields'; - value: number | ArrayField; + value: string | ArrayField; } )[] | null; - relationshipDrawerWithAllowCreateFalse?: (number | null) | TextField; + relationshipDrawerWithAllowCreateFalse?: (string | null) | TextField; relationshipDrawerWithFilterOptions?: { relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | null; updatedAt: string; createdAt: string; @@ -1406,7 +1406,7 @@ export interface RelationshipField { * via the `definition` "select-fields". */ export interface SelectField { - id: number; + id: string; select?: ('one' | 'two' | 'three') | null; selectReadOnly?: ('one' | 'two' | 'three') | null; selectHasMany?: ('one' | 'two' | 'three' | 'four' | 'five' | 'six')[] | null; @@ -1436,7 +1436,7 @@ export interface SelectField { * via the `definition` "tabs-fields-2". */ export interface TabsFields2 { - id: number; + id: string; tabsInArray?: | { text?: string | null; @@ -1454,7 +1454,7 @@ export interface TabsFields2 { * via the `definition` "tabs-fields". */ export interface TabsField { - id: number; + id: string; /** * This should not collapse despite there being many tabs pushing the main fields open. */ @@ -1556,9 +1556,9 @@ export interface TabWithName { * via the `definition` "uploads". */ export interface Upload { - id: number; + id: string; text?: string | null; - media?: (number | null) | Upload; + media?: (string | null) | Upload; updatedAt: string; createdAt: string; url?: string | null; @@ -1576,9 +1576,9 @@ export interface Upload { * via the `definition` "uploads2". */ export interface Uploads2 { - id: number; + id: string; text?: string | null; - media?: (number | null) | Uploads2; + media?: (string | null) | Uploads2; updatedAt: string; createdAt: string; url?: string | null; @@ -1596,8 +1596,8 @@ export interface Uploads2 { * via the `definition` "uploads3". */ export interface Uploads3 { - id: number; - media?: (number | null) | Uploads3; + id: string; + media?: (string | null) | Uploads3; updatedAt: string; createdAt: string; url?: string | null; @@ -1615,9 +1615,9 @@ export interface Uploads3 { * via the `definition` "uploads-multi". */ export interface UploadsMulti { - id: number; + id: string; text?: string | null; - media?: (number | Upload)[] | null; + media?: (string | Upload)[] | null; updatedAt: string; createdAt: string; } @@ -1626,16 +1626,16 @@ export interface UploadsMulti { * via the `definition` "uploads-poly". */ export interface UploadsPoly { - id: number; + id: string; text?: string | null; media?: | ({ relationTo: 'uploads'; - value: number | Upload; + value: string | Upload; } | null) | ({ relationTo: 'uploads2'; - value: number | Uploads2; + value: string | Uploads2; } | null); updatedAt: string; createdAt: string; @@ -1645,17 +1645,17 @@ export interface UploadsPoly { * via the `definition` "uploads-multi-poly". */ export interface UploadsMultiPoly { - id: number; + id: string; text?: string | null; media?: | ( | { relationTo: 'uploads'; - value: number | Upload; + value: string | Upload; } | { relationTo: 'uploads2'; - value: number | Uploads2; + value: string | Uploads2; } )[] | null; @@ -1667,11 +1667,11 @@ export interface UploadsMultiPoly { * via the `definition` "uploads-restricted". */ export interface UploadsRestricted { - id: number; + id: string; text?: string | null; - uploadWithoutRestriction?: (number | null) | Upload; - uploadWithAllowCreateFalse?: (number | null) | Upload; - uploadMultipleWithAllowCreateFalse?: (number | Upload)[] | null; + uploadWithoutRestriction?: (string | null) | Upload; + uploadWithAllowCreateFalse?: (string | null) | Upload; + uploadMultipleWithAllowCreateFalse?: (string | Upload)[] | null; updatedAt: string; createdAt: string; } @@ -1680,7 +1680,7 @@ export interface UploadsRestricted { * via the `definition` "ui-fields". */ export interface UiField { - id: number; + id: string; text: string; updatedAt: string; createdAt: string; @@ -1690,39 +1690,39 @@ export interface UiField { * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: number; + id: string; document?: | ({ relationTo: 'users'; - value: number | User; + value: string | User; } | null) | ({ relationTo: 'select-versions-fields'; - value: number | SelectVersionsField; + value: string | SelectVersionsField; } | null) | ({ relationTo: 'array-fields'; - value: number | ArrayField; + value: string | ArrayField; } | null) | ({ relationTo: 'block-fields'; - value: number | BlockField; + value: string | BlockField; } | null) | ({ relationTo: 'checkbox-fields'; - value: number | CheckboxField; + value: string | CheckboxField; } | null) | ({ relationTo: 'code-fields'; - value: number | CodeField; + value: string | CodeField; } | null) | ({ relationTo: 'collapsible-fields'; - value: number | CollapsibleField; + value: string | CollapsibleField; } | null) | ({ relationTo: 'conditional-logic'; - value: number | ConditionalLogic; + value: string | ConditionalLogic; } | null) | ({ relationTo: 'custom-id'; @@ -1730,27 +1730,27 @@ export interface PayloadLockedDocument { } | null) | ({ relationTo: 'custom-tab-id'; - value: number | CustomTabId; + value: string | CustomTabId; } | null) | ({ relationTo: 'custom-row-id'; - value: number | CustomRowId; + value: string | CustomRowId; } | null) | ({ relationTo: 'date-fields'; - value: number | DateField; + value: string | DateField; } | null) | ({ relationTo: 'email-fields'; - value: number | EmailField; + value: string | EmailField; } | null) | ({ relationTo: 'radio-fields'; - value: number | RadioField; + value: string | RadioField; } | null) | ({ relationTo: 'group-fields'; - value: number | GroupField; + value: string | GroupField; } | null) | ({ relationTo: 'row-fields'; @@ -1758,76 +1758,76 @@ export interface PayloadLockedDocument { } | null) | ({ relationTo: 'indexed-fields'; - value: number | IndexedField; + value: string | IndexedField; } | null) | ({ relationTo: 'json-fields'; - value: number | JsonField; + value: string | JsonField; } | null) | ({ relationTo: 'number-fields'; - value: number | NumberField; + value: string | NumberField; } | null) | ({ relationTo: 'point-fields'; - value: number | PointField; + value: string | PointField; } | null) | ({ relationTo: 'relationship-fields'; - value: number | RelationshipField; + value: string | RelationshipField; } | null) | ({ relationTo: 'select-fields'; - value: number | SelectField; + value: string | SelectField; } | null) | ({ relationTo: 'tabs-fields-2'; - value: number | TabsFields2; + value: string | TabsFields2; } | null) | ({ relationTo: 'tabs-fields'; - value: number | TabsField; + value: string | TabsField; } | null) | ({ relationTo: 'text-fields'; - value: number | TextField; + value: string | TextField; } | null) | ({ relationTo: 'uploads'; - value: number | Upload; + value: string | Upload; } | null) | ({ relationTo: 'uploads2'; - value: number | Uploads2; + value: string | Uploads2; } | null) | ({ relationTo: 'uploads3'; - value: number | Uploads3; + value: string | Uploads3; } | null) | ({ relationTo: 'uploads-multi'; - value: number | UploadsMulti; + value: string | UploadsMulti; } | null) | ({ relationTo: 'uploads-poly'; - value: number | UploadsPoly; + value: string | UploadsPoly; } | null) | ({ relationTo: 'uploads-multi-poly'; - value: number | UploadsMultiPoly; + value: string | UploadsMultiPoly; } | null) | ({ relationTo: 'uploads-restricted'; - value: number | UploadsRestricted; + value: string | UploadsRestricted; } | null) | ({ relationTo: 'ui-fields'; - value: number | UiField; + value: string | UiField; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; updatedAt: string; createdAt: string; @@ -1837,10 +1837,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: number; + id: string; user: { relationTo: 'users'; - value: number | User; + value: string | User; }; key?: string | null; value?: @@ -1860,7 +1860,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: number; + id: string; name?: string | null; batch?: number | null; updatedAt: string; diff --git a/test/form-state/collections/Posts/ArrayRowLabel.tsx b/test/form-state/collections/Posts/ArrayRowLabel.tsx index 22a5c9813b..38f6b4b893 100644 --- a/test/form-state/collections/Posts/ArrayRowLabel.tsx +++ b/test/form-state/collections/Posts/ArrayRowLabel.tsx @@ -1,5 +1,9 @@ import React from 'react' -export const ArrayRowLabel = () => { - return

This is a custom component

+export const ArrayRowLabel = (props) => { + return ( +

+ This is a custom component +

+ ) } diff --git a/test/form-state/e2e.spec.ts b/test/form-state/e2e.spec.ts index 27fb396099..cc9bfafbba 100644 --- a/test/form-state/e2e.spec.ts +++ b/test/form-state/e2e.spec.ts @@ -6,7 +6,12 @@ import { expect, test } from '@playwright/test' import { assertElementStaysVisible } from 'helpers/e2e/assertElementStaysVisible.js' import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js' import { assertRequestBody } from 'helpers/e2e/assertRequestBody.js' -import { addArrayRowAsync, removeArrayRow } from 'helpers/e2e/fields/array/index.js' +import { + addArrayRow, + addArrayRowAsync, + duplicateArrayRow, + removeArrayRow, +} from 'helpers/e2e/fields/array/index.js' import { addBlock } from 'helpers/e2e/fields/blocks/index.js' import { waitForAutoSaveToRunAndComplete } from 'helpers/e2e/waitForAutoSaveToRunAndComplete.js' import * as path from 'path' @@ -454,6 +459,34 @@ test.describe('Form State', () => { await expect(computedTitleField).toHaveValue('Test Title 2') }) + test('array and block rows and maintain consistent row IDs across duplication', async () => { + await page.goto(postsUrl.create) + await addArrayRow(page, { fieldName: 'array' }) + + const row0 = page.locator('#field-array #array-row-0') + + await expect(row0.locator('#custom-array-row-label')).toHaveAttribute('data-id') + + await expect(row0.locator('#field-array__0__id')).toHaveValue( + (await row0.locator('#custom-array-row-label').getAttribute('data-id'))!, + ) + + await duplicateArrayRow(page, { fieldName: 'array' }) + + const row1 = page.locator('#field-array #array-row-1') + + await expect(row1.locator('#custom-array-row-label')).toHaveAttribute('data-id') + + await expect(row1.locator('#custom-array-row-label')).not.toHaveAttribute( + 'data-id', + (await row0.locator('#custom-array-row-label').getAttribute('data-id'))!, + ) + + await expect(row1.locator('#field-array__1__id')).toHaveValue( + (await row1.locator('#custom-array-row-label').getAttribute('data-id'))!, + ) + }) + describe('Throttled tests', () => { let cdpSession: CDPSession diff --git a/test/helpers/e2e/fields/array/addArrayRow.ts b/test/helpers/e2e/fields/array/addArrayRow.ts index e47e513677..5fc740492a 100644 --- a/test/helpers/e2e/fields/array/addArrayRow.ts +++ b/test/helpers/e2e/fields/array/addArrayRow.ts @@ -1,6 +1,7 @@ import type { Locator, Page } from 'playwright' import { wait } from 'payload/shared' +import { expect } from 'playwright/test' import { openArrayRowActions } from './openArrayRowActions.js' @@ -18,10 +19,12 @@ export const addArrayRow = async ( page: Page, { fieldName }: Omit[1], 'rowIndex'>, ) => { + const rowLocator = page.locator(`#field-${fieldName} .array-field__row`) + const numberOfPrevRows = await rowLocator.count() + await addArrayRowAsync(page, fieldName) - // TODO: test the array row has appeared - await wait(300) + expect(await rowLocator.count()).toBe(numberOfPrevRows + 1) } /** @@ -31,16 +34,19 @@ export const addArrayRowBelow = async ( page: Page, { fieldName, rowIndex = 0 }: Parameters[1], ): Promise<{ popupContentLocator: Locator; rowActionsButtonLocator: Locator }> => { + const rowLocator = page.locator(`#field-${fieldName} .array-field__row`) + const numberOfPrevRows = await rowLocator.count() + const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, { fieldName, rowIndex, }) - const addBelowButton = popupContentLocator.locator('.array-actions__action.array-actions__add') + await popupContentLocator.locator('.array-actions__action.array-actions__add').click() - await addBelowButton.click() + await expect(rowLocator).toHaveCount(numberOfPrevRows + 1) - // TODO: test the array row has appeared + // TODO: test the array row has appeared in the _correct position_ (immediately below the original row) await wait(300) return { popupContentLocator, rowActionsButtonLocator } diff --git a/test/helpers/e2e/fields/array/duplicateArrayRow.ts b/test/helpers/e2e/fields/array/duplicateArrayRow.ts index 4019a13a3c..0f7d87a14a 100644 --- a/test/helpers/e2e/fields/array/duplicateArrayRow.ts +++ b/test/helpers/e2e/fields/array/duplicateArrayRow.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from 'playwright' +import { expect } from 'playwright/test' + import { openArrayRowActions } from './openArrayRowActions.js' /** @@ -12,6 +14,9 @@ export const duplicateArrayRow = async ( popupContentLocator: Locator rowActionsButtonLocator: Locator }> => { + const rowLocator = page.locator(`#field-${fieldName} .array-field__row`) + const numberOfPrevRows = await rowLocator.count() + const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, { fieldName, rowIndex, @@ -19,7 +24,9 @@ export const duplicateArrayRow = async ( await popupContentLocator.locator('.array-actions__action.array-actions__duplicate').click() - // TODO: test the array row has been duplicated + expect(await rowLocator.count()).toBe(numberOfPrevRows + 1) + + // TODO: test the array row's field input values have been duplicated as well return { popupContentLocator, rowActionsButtonLocator } } diff --git a/test/helpers/e2e/fields/array/removeArrayRow.ts b/test/helpers/e2e/fields/array/removeArrayRow.ts index 0d69b4cb8f..dfc028de58 100644 --- a/test/helpers/e2e/fields/array/removeArrayRow.ts +++ b/test/helpers/e2e/fields/array/removeArrayRow.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from 'playwright' +import { expect } from 'playwright/test' + import { openArrayRowActions } from './openArrayRowActions.js' /** @@ -12,6 +14,9 @@ export const removeArrayRow = async ( popupContentLocator: Locator rowActionsButtonLocator: Locator }> => { + const rowLocator = page.locator(`#field-${fieldName} .array-field__row`) + const numberOfPrevRows = await rowLocator.count() + const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, { fieldName, rowIndex, @@ -19,8 +24,10 @@ export const removeArrayRow = async ( await popupContentLocator.locator('.array-actions__action.array-actions__remove').click() - // TODO: test the array row has been removed - // another row may have been moved into its place, though + expect(await rowLocator.count()).toBe(numberOfPrevRows - 1) + + // TODO: test the array row has been removed in the _correct position_ (original row index) + // another row may have been moved into its place, need to ensure the test accounts for this fact return { popupContentLocator, rowActionsButtonLocator } } diff --git a/test/helpers/e2e/fields/blocks/addBlock.ts b/test/helpers/e2e/fields/blocks/addBlock.ts index b086f0868d..e6c76a23fe 100644 --- a/test/helpers/e2e/fields/blocks/addBlock.ts +++ b/test/helpers/e2e/fields/blocks/addBlock.ts @@ -1,10 +1,30 @@ -import type { Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' import { expect } from '@playwright/test' import { exactText } from 'helpers.js' +import { openArrayRowActions } from '../array/openArrayRowActions.js' import { openBlocksDrawer } from './openBlocksDrawer.js' +const selectBlockFromDrawer = async ({ + blocksDrawer, + blockToSelect, +}: { + blocksDrawer: Locator + blockToSelect: string +}) => { + const blockCard = blocksDrawer.locator('.blocks-drawer__block .thumbnail-card__label', { + hasText: blockToSelect, + }) + + await expect(blockCard).toBeVisible() + + await blocksDrawer.getByRole('button', { name: exactText(blockToSelect) }).click() +} + +/** + * Adds a block to the end of the blocks array using the primary "Add Block" button. + */ export const addBlock = async ({ page, fieldName = 'blocks', @@ -17,15 +37,66 @@ export const addBlock = async ({ fieldName: string page: Page }) => { + const rowLocator = page.locator( + `#field-${fieldName} > .blocks-field__rows > div > .blocks-field__row`, + ) + + const numberOfPrevRows = await rowLocator.count() + const blocksDrawer = await openBlocksDrawer({ page, fieldName }) - const blockCard = blocksDrawer.locator('.blocks-drawer__block .thumbnail-card__label', { - hasText: blockToSelect, + await selectBlockFromDrawer({ + blocksDrawer, + blockToSelect, }) - await expect(blockCard).toBeVisible() - - await blocksDrawer.getByRole('button', { name: exactText(blockToSelect) }).click() + await expect(rowLocator).toHaveCount(numberOfPrevRows + 1) // expect to see the block on the page } + +/** + * Like `addBlock`, but inserts the block at the specified index using the row actions menu. + */ +export const addBlockBelow = async ( + page: Page, + { + fieldName = 'blocks', + blockToSelect = 'Block', + rowIndex = 0, + }: { + /** + * The name of the block to select from the blocks drawer. + */ + blockToSelect: string + fieldName: string + /** + * The index at which to insert the block. + */ + rowIndex?: number + }, +) => { + const rowLocator = page.locator( + `#field-${fieldName} > .blocks-field__rows > div > .blocks-field__row`, + ) + + const numberOfPrevRows = await rowLocator.count() + + const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, { + fieldName, + rowIndex, + }) + + await popupContentLocator.locator('.array-actions__action.array-actions__add').click() + + const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]') + + await selectBlockFromDrawer({ + blocksDrawer, + blockToSelect, + }) + + await expect(rowLocator).toHaveCount(numberOfPrevRows + 1) + + return { popupContentLocator, rowActionsButtonLocator } +} diff --git a/test/helpers/e2e/fields/blocks/duplicateBlock.ts b/test/helpers/e2e/fields/blocks/duplicateBlock.ts new file mode 100644 index 0000000000..db2c9a2250 --- /dev/null +++ b/test/helpers/e2e/fields/blocks/duplicateBlock.ts @@ -0,0 +1,37 @@ +import type { Locator, Page } from 'playwright' + +import { expect } from 'playwright/test' + +import { openArrayRowActions } from '../array/openArrayRowActions.js' + +/** + * Duplicates the block row at the specified index. + */ +export const duplicateBlock = async ( + page: Page, + { fieldName, rowIndex = 0 }: Parameters[1], +): Promise<{ + popupContentLocator: Locator + rowActionsButtonLocator: Locator + rowCount: number +}> => { + const rowLocator = page.locator( + `#field-${fieldName} > .blocks-field__rows > div > .blocks-field__row`, + ) + + const numberOfPrevRows = await rowLocator.count() + + const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, { + fieldName, + rowIndex, + }) + + await popupContentLocator.locator('.array-actions__action.array-actions__duplicate').click() + const numberOfCurrentRows = await rowLocator.count() + + expect(numberOfCurrentRows).toBe(numberOfPrevRows + 1) + + // TODO: test the array row's field input values have been duplicated as well + + return { popupContentLocator, rowActionsButtonLocator, rowCount: numberOfCurrentRows } +} diff --git a/test/helpers/e2e/fields/blocks/index.ts b/test/helpers/e2e/fields/blocks/index.ts index 5b59954d0e..88792f34b6 100644 --- a/test/helpers/e2e/fields/blocks/index.ts +++ b/test/helpers/e2e/fields/blocks/index.ts @@ -1,4 +1,5 @@ -export { addBlock } from './addBlock.js' +export { addBlock, addBlockBelow } from './addBlock.js' +export { duplicateBlock } from './duplicateBlock.js' export { openBlocksDrawer } from './openBlocksDrawer.js' export { removeAllBlocks } from './removeAllBlocks.js' export { reorderBlocks } from './reorderBlocks.js'