diff --git a/packages/drizzle/src/transform/read/traverseFields.ts b/packages/drizzle/src/transform/read/traverseFields.ts index 261336a30..f32035311 100644 --- a/packages/drizzle/src/transform/read/traverseFields.ts +++ b/packages/drizzle/src/transform/read/traverseFields.ts @@ -98,6 +98,8 @@ export const traverseFields = >({ withinArrayOrBlockLocale, }: TraverseFieldsArgs): T => { const sanitizedPath = path ? `${path}.` : path + const localeCodes = + adapter.payload.config.localization && adapter.payload.config.localization.localeCodes const formatted = fields.reduce((result, field) => { if (fieldIsVirtual(field)) { @@ -506,6 +508,10 @@ export const traverseFields = >({ if (field.type === 'text' && field?.hasMany) { const textPathMatch = texts[`${sanitizedPath}${field.name}`] if (!textPathMatch) { + result[field.name] = + isLocalized && localeCodes + ? Object.fromEntries(localeCodes.map((locale) => [locale, []])) + : [] return result } @@ -545,6 +551,10 @@ export const traverseFields = >({ if (field.type === 'number' && field.hasMany) { const numberPathMatch = numbers[`${sanitizedPath}${field.name}`] if (!numberPathMatch) { + result[field.name] = + isLocalized && localeCodes + ? Object.fromEntries(localeCodes.map((locale) => [locale, []])) + : [] return result } @@ -606,10 +616,8 @@ export const traverseFields = >({ } if (isLocalized && Array.isArray(table._locales)) { - if (!table._locales.length && adapter.payload.config.localization) { - adapter.payload.config.localization.localeCodes.forEach((_locale) => - (table._locales as unknown[]).push({ _locale }), - ) + if (!table._locales.length && localeCodes) { + localeCodes.forEach((_locale) => (table._locales as unknown[]).push({ _locale })) } table._locales.forEach((localeRow) => { @@ -725,8 +733,6 @@ export const traverseFields = >({ } return result - - return result }, dataRef) if (Array.isArray(table._locales)) { diff --git a/packages/drizzle/src/transform/write/array.ts b/packages/drizzle/src/transform/write/array.ts index b6d7fad48..522400cc7 100644 --- a/packages/drizzle/src/transform/write/array.ts +++ b/packages/drizzle/src/transform/write/array.ts @@ -3,7 +3,13 @@ import type { FlattenedArrayField } from 'payload' import { fieldShouldBeLocalized } from 'payload/shared' import type { DrizzleAdapter } from '../../types.js' -import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js' +import type { + ArrayRowToInsert, + BlockRowToInsert, + NumberToDelete, + RelationshipToDelete, + TextToDelete, +} from './types.js' import { isArrayOfRows } from '../../utilities/isArrayOfRows.js' import { traverseFields } from './traverseFields.js' @@ -20,6 +26,7 @@ type Args = { field: FlattenedArrayField locale?: string numbers: Record[] + numbersToDelete: NumberToDelete[] parentIsLocalized: boolean path: string relationships: Record[] @@ -28,6 +35,7 @@ type Args = { [tableName: string]: Record[] } texts: Record[] + textsToDelete: TextToDelete[] /** * Set to a locale code if this set of fields is traversed within a * localized array or block field @@ -45,12 +53,14 @@ export const transformArray = ({ field, locale, numbers, + numbersToDelete, parentIsLocalized, path, relationships, relationshipsToDelete, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }: Args) => { const newRows: ArrayRowToInsert[] = [] @@ -104,6 +114,7 @@ export const transformArray = ({ insideArrayOrBlock: true, locales: newRow.locales, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, parentTableName: arrayTableName, path: `${path || ''}${field.name}.${i}.`, @@ -112,6 +123,7 @@ export const transformArray = ({ row: newRow.row, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }) diff --git a/packages/drizzle/src/transform/write/blocks.ts b/packages/drizzle/src/transform/write/blocks.ts index 1e8c57693..dd7864338 100644 --- a/packages/drizzle/src/transform/write/blocks.ts +++ b/packages/drizzle/src/transform/write/blocks.ts @@ -4,7 +4,12 @@ import { fieldShouldBeLocalized } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { DrizzleAdapter } from '../../types.js' -import type { BlockRowToInsert, RelationshipToDelete } from './types.js' +import type { + BlockRowToInsert, + NumberToDelete, + RelationshipToDelete, + TextToDelete, +} from './types.js' import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js' import { traverseFields } from './traverseFields.js' @@ -20,6 +25,7 @@ type Args = { field: FlattenedBlocksField locale?: string numbers: Record[] + numbersToDelete: NumberToDelete[] parentIsLocalized: boolean path: string relationships: Record[] @@ -28,6 +34,7 @@ type Args = { [tableName: string]: Record[] } texts: Record[] + textsToDelete: TextToDelete[] /** * Set to a locale code if this set of fields is traversed within a * localized array or block field @@ -43,12 +50,14 @@ export const transformBlocks = ({ field, locale, numbers, + numbersToDelete, parentIsLocalized, path, relationships, relationshipsToDelete, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }: Args) => { data.forEach((blockRow, i) => { @@ -117,6 +126,7 @@ export const transformBlocks = ({ insideArrayOrBlock: true, locales: newRow.locales, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, parentTableName: blockTableName, path: `${path || ''}${field.name}.${i}.`, @@ -125,6 +135,7 @@ export const transformBlocks = ({ row: newRow.row, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }) diff --git a/packages/drizzle/src/transform/write/index.ts b/packages/drizzle/src/transform/write/index.ts index 2e45a3b12..e70b91ff8 100644 --- a/packages/drizzle/src/transform/write/index.ts +++ b/packages/drizzle/src/transform/write/index.ts @@ -29,11 +29,13 @@ export const transformForWrite = ({ blocksToDelete: new Set(), locales: {}, numbers: [], + numbersToDelete: [], relationships: [], relationshipsToDelete: [], row: {}, selects: {}, texts: [], + textsToDelete: [], } // This function is responsible for building up the @@ -50,6 +52,7 @@ export const transformForWrite = ({ fields, locales: rowToInsert.locales, numbers: rowToInsert.numbers, + numbersToDelete: rowToInsert.numbersToDelete, parentIsLocalized, parentTableName: tableName, path, @@ -58,6 +61,7 @@ export const transformForWrite = ({ row: rowToInsert.row, selects: rowToInsert.selects, texts: rowToInsert.texts, + textsToDelete: rowToInsert.textsToDelete, }) return rowToInsert diff --git a/packages/drizzle/src/transform/write/traverseFields.ts b/packages/drizzle/src/transform/write/traverseFields.ts index c69ebe8ff..1cf8821f0 100644 --- a/packages/drizzle/src/transform/write/traverseFields.ts +++ b/packages/drizzle/src/transform/write/traverseFields.ts @@ -5,7 +5,13 @@ import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared' import toSnakeCase from 'to-snake-case' import type { DrizzleAdapter } from '../../types.js' -import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js' +import type { + ArrayRowToInsert, + BlockRowToInsert, + NumberToDelete, + RelationshipToDelete, + TextToDelete, +} from './types.js' import { isArrayOfRows } from '../../utilities/isArrayOfRows.js' import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js' @@ -51,6 +57,7 @@ type Args = { [locale: string]: Record } numbers: Record[] + numbersToDelete: NumberToDelete[] parentIsLocalized: boolean /** * This is the name of the parent table @@ -64,6 +71,7 @@ type Args = { [tableName: string]: Record[] } texts: Record[] + textsToDelete: TextToDelete[] /** * Set to a locale code if this set of fields is traversed within a * localized array or block field @@ -86,6 +94,7 @@ export const traverseFields = ({ insideArrayOrBlock = false, locales, numbers, + numbersToDelete, parentIsLocalized, parentTableName, path, @@ -94,6 +103,7 @@ export const traverseFields = ({ row, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }: Args) => { if (row._uuid) { @@ -136,12 +146,14 @@ export const traverseFields = ({ field, locale: localeKey, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, selects, texts, + textsToDelete, withinArrayOrBlockLocale: localeKey, }) @@ -159,12 +171,14 @@ export const traverseFields = ({ data: data[field.name], field, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }) @@ -202,12 +216,14 @@ export const traverseFields = ({ field, locale: localeKey, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, selects, texts, + textsToDelete, withinArrayOrBlockLocale: localeKey, }) } @@ -222,12 +238,14 @@ export const traverseFields = ({ data: fieldData, field, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, path, relationships, relationshipsToDelete, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }) } @@ -257,6 +275,7 @@ export const traverseFields = ({ insideArrayOrBlock, locales, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, parentTableName, path: `${path || ''}${field.name}.`, @@ -265,6 +284,7 @@ export const traverseFields = ({ row, selects, texts, + textsToDelete, withinArrayOrBlockLocale: localeKey, }) }) @@ -287,6 +307,7 @@ export const traverseFields = ({ insideArrayOrBlock, locales, numbers, + numbersToDelete, parentIsLocalized: parentIsLocalized || field.localized, parentTableName, path: `${path || ''}${field.name}.`, @@ -295,6 +316,7 @@ export const traverseFields = ({ row, selects, texts, + textsToDelete, withinArrayOrBlockLocale, }) } @@ -380,6 +402,11 @@ export const traverseFields = ({ if (typeof fieldData === 'object') { Object.entries(fieldData).forEach(([localeKey, localeData]) => { if (Array.isArray(localeData)) { + if (!localeData.length) { + textsToDelete.push({ locale: localeKey, path: textPath }) + return + } + transformTexts({ baseRow: { locale: localeKey, @@ -392,6 +419,11 @@ export const traverseFields = ({ }) } } else if (Array.isArray(fieldData)) { + if (!fieldData.length) { + textsToDelete.push({ locale: withinArrayOrBlockLocale, path: textPath }) + return + } + transformTexts({ baseRow: { locale: withinArrayOrBlockLocale, @@ -412,6 +444,11 @@ export const traverseFields = ({ if (typeof fieldData === 'object') { Object.entries(fieldData).forEach(([localeKey, localeData]) => { if (Array.isArray(localeData)) { + if (!localeData.length) { + numbersToDelete.push({ locale: localeKey, path: numberPath }) + return + } + transformNumbers({ baseRow: { locale: localeKey, @@ -424,6 +461,11 @@ export const traverseFields = ({ }) } } else if (Array.isArray(fieldData)) { + if (!fieldData.length) { + numbersToDelete.push({ locale: withinArrayOrBlockLocale, path: numberPath }) + return + } + transformNumbers({ baseRow: { locale: withinArrayOrBlockLocale, diff --git a/packages/drizzle/src/transform/write/types.ts b/packages/drizzle/src/transform/write/types.ts index 838a08f66..27281a695 100644 --- a/packages/drizzle/src/transform/write/types.ts +++ b/packages/drizzle/src/transform/write/types.ts @@ -23,6 +23,16 @@ export type RelationshipToDelete = { path: string } +export type TextToDelete = { + locale?: string + path: string +} + +export type NumberToDelete = { + locale?: string + path: string +} + export type RowToInsert = { arrays: { [tableName: string]: ArrayRowToInsert[] @@ -35,6 +45,7 @@ export type RowToInsert = { [locale: string]: Record } numbers: Record[] + numbersToDelete: NumberToDelete[] relationships: Record[] relationshipsToDelete: RelationshipToDelete[] row: Record @@ -42,4 +53,5 @@ export type RowToInsert = { [tableName: string]: Record[] } texts: Record[] + textsToDelete: TextToDelete[] } diff --git a/packages/drizzle/src/upsertRow/index.ts b/packages/drizzle/src/upsertRow/index.ts index fb8bd26a2..3337a7127 100644 --- a/packages/drizzle/src/upsertRow/index.ts +++ b/packages/drizzle/src/upsertRow/index.ts @@ -211,7 +211,7 @@ export const upsertRow = async | TypeWithID>( parentColumnName: 'parent', parentID: insertedRow.id, pathColumnName: 'path', - rows: textsToInsert, + rows: [...textsToInsert, ...rowToInsert.textsToDelete], tableName: textsTableName, }) } @@ -238,7 +238,7 @@ export const upsertRow = async | TypeWithID>( parentColumnName: 'parent', parentID: insertedRow.id, pathColumnName: 'path', - rows: numbersToInsert, + rows: [...numbersToInsert, ...rowToInsert.numbersToDelete], tableName: numbersTableName, }) } diff --git a/packages/ui/src/fields/Number/index.tsx b/packages/ui/src/fields/Number/index.tsx index c7f135a26..660a1b719 100644 --- a/packages/ui/src/fields/Number/index.tsx +++ b/packages/ui/src/fields/Number/index.tsx @@ -26,7 +26,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => { admin: { className, description, - placeholder, + placeholder: placeholderFromProps, step = 1, } = {} as NumberFieldClientProps['field']['admin'], hasMany = false, @@ -126,6 +126,8 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => { const styles = useMemo(() => mergeFieldStyles(field), [field]) + const placeholder = getTranslation(placeholderFromProps, i18n) + return (
{ // numberOnly onChange={handleHasManyChange} options={[]} - placeholder={t('general:enterAValue')} + placeholder={placeholder} showError={showError} value={valueToRender as Option[]} /> @@ -191,7 +193,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => { // @ts-expect-error e.target.blur() }} - placeholder={getTranslation(placeholder, i18n)} + placeholder={placeholder} step={step} type="number" value={typeof value === 'number' ? value : ''} diff --git a/packages/ui/src/fields/Text/Input.tsx b/packages/ui/src/fields/Text/Input.tsx index 8e24f4c21..679ccdef9 100644 --- a/packages/ui/src/fields/Text/Input.tsx +++ b/packages/ui/src/fields/Text/Input.tsx @@ -34,7 +34,7 @@ export const TextInput: React.FC = (props) => { onChange, onKeyDown, path, - placeholder, + placeholder: placeholderFromProps, readOnly, required, rtl, @@ -91,6 +91,8 @@ export const TextInput: React.FC = (props) => { } } + const placeholder = getTranslation(placeholderFromProps, i18n) + return (
= (props) => { }} onChange={onChange} options={[]} - placeholder={t('general:enterAValue')} + placeholder={placeholder} showError={showError} value={valueToRender} /> @@ -155,7 +157,7 @@ export const TextInput: React.FC = (props) => { name={path} onChange={onChange as (e: ChangeEvent) => void} onKeyDown={onKeyDown} - placeholder={getTranslation(placeholder, i18n)} + placeholder={placeholder} ref={inputRef} type="text" value={value || ''} diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 3dca326c0..61fa97715 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -33,6 +33,7 @@ import { checkboxFieldsSlug, collapsibleFieldsSlug, groupFieldsSlug, + numberFieldsSlug, relationshipFieldsSlug, tabsFieldsSlug, textFieldsSlug, @@ -401,6 +402,31 @@ describe('Fields', () => { expect(resInSecond.totalDocs).toBe(1) }) + + it('should delete rows when updating hasMany with empty array', async () => { + const { id: createdDocId } = await payload.create({ + collection: textFieldsSlug, + data: { + text: 'hasMany deletion test', + hasMany: ['one', 'two', 'three'], + }, + }) + + await payload.update({ + collection: textFieldsSlug, + id: createdDocId, + data: { + hasMany: [], + }, + }) + + const resultingDoc = await payload.findByID({ + collection: textFieldsSlug, + id: createdDocId, + }) + + expect(resultingDoc.hasMany).toHaveLength(0) + }) }) describe('relationship', () => { @@ -1042,6 +1068,30 @@ describe('Fields', () => { expect(numbersNotExists.docs).toHaveLength(1) }) + + it('should delete rows when updating hasMany with empty array', async () => { + const { id: createdDocId } = await payload.create({ + collection: numberFieldsSlug, + data: { + localizedHasMany: [1, 2, 3], + }, + }) + + await payload.update({ + collection: numberFieldsSlug, + id: createdDocId, + data: { + localizedHasMany: [], + }, + }) + + const resultingDoc = await payload.findByID({ + collection: numberFieldsSlug, + id: createdDocId, + }) + + expect(resultingDoc.localizedHasMany).toHaveLength(0) + }) }) it('should query hasMany within an array', async () => {