fix: duplication with localized arrays in unnamed tabs (#8236)
Fixes a case where in relational DBs, you can't duplicate documents if you have localized arrays within unnamed tabs. The `beforeDuplicate` hooks were not being run for fields within unnamed tabs.
This commit is contained in:
@@ -40,6 +40,24 @@ export const promise = async <T>({
|
|||||||
parentSchemaPath,
|
parentSchemaPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle unnamed tabs
|
||||||
|
if (field.type === 'tab' && !tabHasName(field)) {
|
||||||
|
await traverseFields({
|
||||||
|
id,
|
||||||
|
collection,
|
||||||
|
context,
|
||||||
|
doc,
|
||||||
|
fields: field.fields,
|
||||||
|
overrideAccess,
|
||||||
|
path: fieldPath,
|
||||||
|
req,
|
||||||
|
schemaPath: fieldSchemaPath,
|
||||||
|
siblingDoc,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (fieldAffectsData(field)) {
|
if (fieldAffectsData(field)) {
|
||||||
let fieldData = siblingDoc?.[field.name]
|
let fieldData = siblingDoc?.[field.name]
|
||||||
const fieldIsLocalized = field.localized && localization
|
const fieldIsLocalized = field.localized && localization
|
||||||
@@ -207,20 +225,6 @@ export const promise = async <T>({
|
|||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'tab':
|
case 'tab':
|
||||||
case 'group': {
|
case 'group': {
|
||||||
if (field.type === 'tab' && !tabHasName(field)) {
|
|
||||||
await traverseFields({
|
|
||||||
id,
|
|
||||||
collection,
|
|
||||||
context,
|
|
||||||
doc,
|
|
||||||
fields: field.fields,
|
|
||||||
overrideAccess,
|
|
||||||
path: fieldPath,
|
|
||||||
req,
|
|
||||||
schemaPath: fieldSchemaPath,
|
|
||||||
siblingDoc,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (typeof siblingDoc[field.name] !== 'object') {
|
if (typeof siblingDoc[field.name] !== 'object') {
|
||||||
siblingDoc[field.name] = {}
|
siblingDoc[field.name] = {}
|
||||||
}
|
}
|
||||||
@@ -239,7 +243,6 @@ export const promise = async <T>({
|
|||||||
schemaPath: fieldSchemaPath,
|
schemaPath: fieldSchemaPath,
|
||||||
siblingDoc: groupDoc as JsonObject,
|
siblingDoc: groupDoc as JsonObject,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,16 @@ export default buildConfigWithDefaults({
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
label: 'Main Nav',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'nav',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'layout',
|
name: 'layout',
|
||||||
blocks: [
|
blocks: [
|
||||||
@@ -139,6 +149,28 @@ export default buildConfigWithDefaults({
|
|||||||
name: 'text',
|
name: 'text',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'l2',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'l3',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'l4',
|
||||||
|
type: 'array',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'superNestedText',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -158,9 +190,10 @@ export default buildConfigWithDefaults({
|
|||||||
required: true,
|
required: true,
|
||||||
type: 'blocks',
|
type: 'blocks',
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
type: 'tabs',
|
},
|
||||||
tabs: [
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'myTab',
|
name: 'myTab',
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -205,10 +205,10 @@ describe('Localization', () => {
|
|||||||
await page.goto(urlWithRequiredLocalizedFields.create)
|
await page.goto(urlWithRequiredLocalizedFields.create)
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
await page.locator('#field-title').fill(englishTitle)
|
await page.locator('#field-title').fill(englishTitle)
|
||||||
await page.locator('#field-layout .blocks-field__drawer-toggler').click()
|
await page.locator('#field-nav__layout .blocks-field__drawer-toggler').click()
|
||||||
await page.locator('button[title="Text"]').click()
|
await page.locator('button[title="Text"]').click()
|
||||||
await page.fill('#field-layout__0__text', 'test')
|
await page.fill('#field-nav__layout__0__text', 'test')
|
||||||
await expect(page.locator('#field-layout__0__text')).toHaveValue('test')
|
await expect(page.locator('#field-nav__layout__0__text')).toHaveValue('test')
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
const originalID = await page.locator('.id-label').innerText()
|
const originalID = await page.locator('.id-label').innerText()
|
||||||
await openDocControls(page)
|
await openDocControls(page)
|
||||||
@@ -256,6 +256,10 @@ describe('Localization', () => {
|
|||||||
async function fillValues(data: Partial<LocalizedPost>) {
|
async function fillValues(data: Partial<LocalizedPost>) {
|
||||||
const { description: descVal, title: titleVal } = data
|
const { description: descVal, title: titleVal } = data
|
||||||
|
|
||||||
if (titleVal) {await page.locator('#field-title').fill(titleVal)}
|
if (titleVal) {
|
||||||
if (descVal) {await page.locator('#field-description').fill(descVal)}
|
await page.locator('#field-title').fill(titleVal)
|
||||||
|
}
|
||||||
|
if (descVal) {
|
||||||
|
await page.locator('#field-description').fill(descVal)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -709,12 +709,14 @@ describe('Localization', () => {
|
|||||||
const newDoc = await payload.create({
|
const newDoc = await payload.create({
|
||||||
collection: withRequiredLocalizedFields,
|
collection: withRequiredLocalizedFields,
|
||||||
data: {
|
data: {
|
||||||
|
nav: {
|
||||||
layout: [
|
layout: [
|
||||||
{
|
{
|
||||||
blockType: 'text',
|
blockType: 'text',
|
||||||
text: 'laiwejfilwaje',
|
text: 'laiwejfilwaje',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -723,12 +725,14 @@ describe('Localization', () => {
|
|||||||
id: newDoc.id,
|
id: newDoc.id,
|
||||||
collection: withRequiredLocalizedFields,
|
collection: withRequiredLocalizedFields,
|
||||||
data: {
|
data: {
|
||||||
|
nav: {
|
||||||
layout: [
|
layout: [
|
||||||
{
|
{
|
||||||
blockType: 'number',
|
blockType: 'number',
|
||||||
number: 12,
|
number: 12,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
title: 'en espanol, big bird',
|
title: 'en espanol, big bird',
|
||||||
},
|
},
|
||||||
locale: spanishLocale,
|
locale: spanishLocale,
|
||||||
@@ -742,7 +746,7 @@ describe('Localization', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(updatedDoc.layout[0].blockType).toStrictEqual('text')
|
expect(updatedDoc.nav.layout[0].blockType).toStrictEqual('text')
|
||||||
|
|
||||||
const spanishDoc = await payload.findByID({
|
const spanishDoc = await payload.findByID({
|
||||||
id: newDoc.id,
|
id: newDoc.id,
|
||||||
@@ -750,7 +754,7 @@ describe('Localization', () => {
|
|||||||
locale: spanishLocale,
|
locale: spanishLocale,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(spanishDoc.layout[0].blockType).toStrictEqual('number')
|
expect(spanishDoc.nav.layout[0].blockType).toStrictEqual('number')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1126,12 +1130,15 @@ describe('Localization', () => {
|
|||||||
// - and this needs to be done recursively for all block / array fields
|
// - and this needs to be done recursively for all block / array fields
|
||||||
// 2. make sure localized arrays / blocks work inside of localized groups / tabs
|
// 2. make sure localized arrays / blocks work inside of localized groups / tabs
|
||||||
// - this is covered with myTab.group.nestedArray2
|
// - this is covered with myTab.group.nestedArray2
|
||||||
|
// 3. the field schema for `nav` is within an unnamed tab, which tests that we
|
||||||
|
// properly recursively loop through all field structures / types
|
||||||
|
|
||||||
const englishText = 'english'
|
const englishText = 'english'
|
||||||
const spanishText = 'spanish'
|
const spanishText = 'spanish'
|
||||||
const doc = await payload.create({
|
const doc = await payload.create({
|
||||||
collection: withRequiredLocalizedFields,
|
collection: withRequiredLocalizedFields,
|
||||||
data: {
|
data: {
|
||||||
|
nav: {
|
||||||
layout: [
|
layout: [
|
||||||
{
|
{
|
||||||
blockType: 'text',
|
blockType: 'text',
|
||||||
@@ -1139,13 +1146,40 @@ describe('Localization', () => {
|
|||||||
nestedArray: [
|
nestedArray: [
|
||||||
{
|
{
|
||||||
text: 'hello',
|
text: 'hello',
|
||||||
|
l2: [
|
||||||
|
{
|
||||||
|
l3: [
|
||||||
|
{
|
||||||
|
l4: [
|
||||||
|
{
|
||||||
|
superNestedText: 'hello',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'goodbye',
|
text: 'goodbye',
|
||||||
|
l2: [
|
||||||
|
{
|
||||||
|
l3: [
|
||||||
|
{
|
||||||
|
l4: [
|
||||||
|
{
|
||||||
|
superNestedText: 'goodbye',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
myTab: {
|
myTab: {
|
||||||
text: 'hello',
|
text: 'hello',
|
||||||
group: {
|
group: {
|
||||||
@@ -1169,6 +1203,7 @@ describe('Localization', () => {
|
|||||||
id: doc.id,
|
id: doc.id,
|
||||||
collection: withRequiredLocalizedFields,
|
collection: withRequiredLocalizedFields,
|
||||||
data: {
|
data: {
|
||||||
|
nav: {
|
||||||
layout: [
|
layout: [
|
||||||
{
|
{
|
||||||
blockType: 'text',
|
blockType: 'text',
|
||||||
@@ -1176,13 +1211,40 @@ describe('Localization', () => {
|
|||||||
nestedArray: [
|
nestedArray: [
|
||||||
{
|
{
|
||||||
text: 'hola',
|
text: 'hola',
|
||||||
|
l2: [
|
||||||
|
{
|
||||||
|
l3: [
|
||||||
|
{
|
||||||
|
l4: [
|
||||||
|
{
|
||||||
|
superNestedText: 'hola',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'adios',
|
text: 'adios',
|
||||||
|
l2: [
|
||||||
|
{
|
||||||
|
l3: [
|
||||||
|
{
|
||||||
|
l4: [
|
||||||
|
{
|
||||||
|
superNestedText: 'adios',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
myTab: {
|
myTab: {
|
||||||
text: 'hola',
|
text: 'hola',
|
||||||
@@ -1215,10 +1277,10 @@ describe('Localization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// check fields
|
// check fields
|
||||||
expect(result.layout[0].text).toStrictEqual(englishText)
|
expect(result.nav.layout[0].text).toStrictEqual(englishText)
|
||||||
|
|
||||||
expect(allLocales.layout.en[0].text).toStrictEqual(englishText)
|
expect(allLocales.nav.layout.en[0].text).toStrictEqual(englishText)
|
||||||
expect(allLocales.layout.es[0].text).toStrictEqual(spanishText)
|
expect(allLocales.nav.layout.es[0].text).toStrictEqual(spanishText)
|
||||||
|
|
||||||
expect(allLocales.myTab.group.en.nestedText).toStrictEqual('hello')
|
expect(allLocales.myTab.group.en.nestedText).toStrictEqual('hello')
|
||||||
expect(allLocales.myTab.group.en.nestedArray2[0].nestedText).toStrictEqual('hello')
|
expect(allLocales.myTab.group.en.nestedArray2[0].nestedText).toStrictEqual('hello')
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface Config {
|
|||||||
tabs: Tab;
|
tabs: Tab;
|
||||||
'localized-sort': LocalizedSort;
|
'localized-sort': LocalizedSort;
|
||||||
'blocks-same-name': BlocksSameName;
|
'blocks-same-name': BlocksSameName;
|
||||||
|
'localized-within-localized': LocalizedWithinLocalized;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
@@ -74,6 +75,14 @@ export interface BlocksField {
|
|||||||
blockType: 'textBlock';
|
blockType: 'textBlock';
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
|
array?:
|
||||||
|
| {
|
||||||
|
link?: {
|
||||||
|
label?: string | null;
|
||||||
|
};
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'blockInsideBlock';
|
blockType: 'blockInsideBlock';
|
||||||
@@ -93,6 +102,9 @@ export interface NestedArray {
|
|||||||
blocksWithinArray?:
|
blocksWithinArray?:
|
||||||
| {
|
| {
|
||||||
relationWithinBlock?: (string | null) | LocalizedPost;
|
relationWithinBlock?: (string | null) | LocalizedPost;
|
||||||
|
myGroup?: {
|
||||||
|
text?: string | null;
|
||||||
|
};
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'someBlock';
|
blockType: 'someBlock';
|
||||||
@@ -109,6 +121,7 @@ export interface NestedArray {
|
|||||||
| null;
|
| null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -195,9 +208,32 @@ export interface ArrayField {
|
|||||||
export interface LocalizedRequired {
|
export interface LocalizedRequired {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
nav: {
|
||||||
layout: (
|
layout: (
|
||||||
| {
|
| {
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
|
nestedArray?:
|
||||||
|
| {
|
||||||
|
text?: string | null;
|
||||||
|
l2?:
|
||||||
|
| {
|
||||||
|
l3?:
|
||||||
|
| {
|
||||||
|
l4?:
|
||||||
|
| {
|
||||||
|
superNestedText?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'text';
|
blockType: 'text';
|
||||||
@@ -209,6 +245,19 @@ export interface LocalizedRequired {
|
|||||||
blockType: 'number';
|
blockType: 'number';
|
||||||
}
|
}
|
||||||
)[];
|
)[];
|
||||||
|
};
|
||||||
|
myTab?: {
|
||||||
|
text?: string | null;
|
||||||
|
group?: {
|
||||||
|
nestedArray2?:
|
||||||
|
| {
|
||||||
|
nestedText?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
nestedText?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -413,6 +462,35 @@ export interface BlocksSameName {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "localized-within-localized".
|
||||||
|
*/
|
||||||
|
export interface LocalizedWithinLocalized {
|
||||||
|
id: string;
|
||||||
|
myTab?: {
|
||||||
|
shouldNotBeLocalized?: string | null;
|
||||||
|
};
|
||||||
|
myArray?:
|
||||||
|
| {
|
||||||
|
shouldNotBeLocalized?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
myBlocks?:
|
||||||
|
| {
|
||||||
|
shouldNotBeLocalized?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'myBlock';
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
myGroup?: {
|
||||||
|
shouldNotBeLocalized?: string | null;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-preferences".
|
* via the `definition` "payload-preferences".
|
||||||
|
|||||||
Reference in New Issue
Block a user