chore: bump all eslint dependencies, run lint and prettier (#9128)

This fixes a peer dependency error in our monorepo, as
eslint-plugin-jsx-a11y finally supports eslint v9.

Additionally, this officially adds TypeScript 5.6 support for
typescript-eslint.
This commit is contained in:
Alessio Gravili
2024-11-12 08:18:22 -07:00
committed by GitHub
parent 3298113a93
commit 03291472d6
64 changed files with 2649 additions and 2810 deletions

View File

@@ -32,13 +32,13 @@ type InitNextArgs = {
} & Pick<CliArgs, '--debug'> } & Pick<CliArgs, '--debug'>
type InitNextResult = type InitNextResult =
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
| { | {
isSrcDir: boolean isSrcDir: boolean
nextAppDir: string nextAppDir: string
payloadConfigPath: string payloadConfigPath: string
success: true success: true
} }
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
export async function initNext(args: InitNextArgs): Promise<InitNextResult> { export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const { dbType: dbType, packageManager, projectDir } = args const { dbType: dbType, packageManager, projectDir } = args

View File

@@ -15,15 +15,9 @@ export async function installPackages(args: {
let stderr = '' let stderr = ''
switch (packageManager) { switch (packageManager) {
case 'npm': { case 'bun':
;({ exitCode, stderr } = await execa('npm', ['install', '--save', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
case 'yarn':
case 'pnpm': case 'pnpm':
case 'bun': { case 'yarn': {
if (packageManager === 'bun') { if (packageManager === 'bun') {
warning('Bun support is untested.') warning('Bun support is untested.')
} }
@@ -32,6 +26,12 @@ export async function installPackages(args: {
})) }))
break break
} }
case 'npm': {
;({ exitCode, stderr } = await execa('npm', ['install', '--save', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
} }
if (exitCode !== 0) { if (exitCode !== 0) {

View File

@@ -217,6 +217,16 @@ export class Main {
} }
switch (template.type) { switch (template.type) {
case 'plugin': {
await createProject({
cliArgs: this.args,
packageManager,
projectDir,
projectName,
template,
})
break
}
case 'starter': { case 'starter': {
const dbDetails = await selectDb(this.args, projectName) const dbDetails = await selectDb(this.args, projectName)
const payloadSecret = generateSecret() const payloadSecret = generateSecret()
@@ -238,16 +248,6 @@ export class Main {
}) })
break break
} }
case 'plugin': {
await createProject({
cliArgs: this.args,
packageManager,
projectDir,
projectName,
template,
})
break
}
} }
info('Payload project successfully created!') info('Payload project successfully created!')

View File

@@ -104,34 +104,10 @@ const traverseFields = ({
} }
switch (field.type) { switch (field.type) {
case 'collapsible': case 'array':
case 'row':
traverseFields({
adapter,
databaseSchemaPath,
fields: field.fields,
projection,
select,
selectMode,
withinLocalizedField,
})
break
case 'tabs':
traverseFields({
adapter,
databaseSchemaPath,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
projection,
select,
selectMode,
withinLocalizedField,
})
break
case 'group': case 'group':
case 'tab':
case 'array': { case 'tab': {
let fieldSelect: SelectType let fieldSelect: SelectType
if (field.type === 'tab' && !tabHasName(field)) { if (field.type === 'tab' && !tabHasName(field)) {
@@ -206,6 +182,30 @@ const traverseFields = ({
break break
} }
case 'collapsible':
case 'row':
traverseFields({
adapter,
databaseSchemaPath,
fields: field.fields,
projection,
select,
selectMode,
withinLocalizedField,
})
break
case 'tabs':
traverseFields({
adapter,
databaseSchemaPath,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
projection,
select,
selectMode,
withinLocalizedField,
})
break
default: default:
break break

View File

@@ -13,44 +13,6 @@ type Args = {
export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
fields.forEach((field) => { fields.forEach((field) => {
switch (field.type) { switch (field.type) {
case 'group': {
const newPath = `${path ? `${path}.` : ''}${field.name}`
const newDoc = doc?.[field.name]
if (typeof newDoc === 'object' && newDoc !== null) {
if (field.localized) {
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
return traverseFields({
doc: localeDoc,
fields: field.fields,
locale,
path: newPath,
rows,
})
})
} else {
return traverseFields({
doc: newDoc as Record<string, unknown>,
fields: field.fields,
path: newPath,
rows,
})
}
}
break
}
case 'row':
case 'collapsible': {
return traverseFields({
doc,
fields: field.fields,
path,
rows,
})
}
case 'array': { case 'array': {
const rowData = doc?.[field.name] const rowData = doc?.[field.name]
@@ -124,19 +86,27 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
break break
} }
case 'collapsible':
// falls through
case 'row': {
return traverseFields({
doc,
fields: field.fields,
path,
rows,
})
}
case 'tabs': { case 'group': {
return field.tabs.forEach((tab) => { const newPath = `${path ? `${path}.` : ''}${field.name}`
if (tabHasName(tab)) { const newDoc = doc?.[field.name]
const newDoc = doc?.[tab.name]
const newPath = `${path ? `${path}.` : ''}${tab.name}`
if (typeof newDoc === 'object' && newDoc !== null) { if (typeof newDoc === 'object' && newDoc !== null) {
if (tab.localized) { if (field.localized) {
Object.entries(newDoc).forEach(([locale, localeDoc]) => { Object.entries(newDoc).forEach(([locale, localeDoc]) => {
return traverseFields({ return traverseFields({
doc: localeDoc, doc: localeDoc,
fields: tab.fields, fields: field.fields,
locale, locale,
path: newPath, path: newPath,
rows, rows,
@@ -145,24 +115,18 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
} else { } else {
return traverseFields({ return traverseFields({
doc: newDoc as Record<string, unknown>, doc: newDoc as Record<string, unknown>,
fields: tab.fields, fields: field.fields,
path: newPath, path: newPath,
rows, rows,
}) })
} }
} }
} else {
traverseFields({ break
doc,
fields: tab.fields,
path,
rows,
})
}
})
} }
case 'relationship': case 'relationship':
// falls through
case 'upload': { case 'upload': {
if (typeof field.relationTo === 'string') { if (typeof field.relationTo === 'string') {
if (field.type === 'upload' || !field.hasMany) { if (field.type === 'upload' || !field.hasMany) {
@@ -211,6 +175,43 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
} }
} }
} }
break
}
case 'tabs': {
return field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
const newDoc = doc?.[tab.name]
const newPath = `${path ? `${path}.` : ''}${tab.name}`
if (typeof newDoc === 'object' && newDoc !== null) {
if (tab.localized) {
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
return traverseFields({
doc: localeDoc,
fields: tab.fields,
locale,
path: newPath,
rows,
})
})
} else {
return traverseFields({
doc: newDoc as Record<string, unknown>,
fields: tab.fields,
path: newPath,
rows,
})
}
}
} else {
traverseFields({
doc,
fields: tab.fields,
path,
rows,
})
}
})
} }
} }
}) })

View File

@@ -27,30 +27,6 @@ type Args = {
export const traverseFields = (args: Args) => { export const traverseFields = (args: Args) => {
args.fields.forEach((field) => { args.fields.forEach((field) => {
switch (field.type) { switch (field.type) {
case 'group': {
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
if (field.localized && args.payload.config.localization) {
newTableName += args.adapter.localesSuffix
}
return traverseFields({
...args,
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
fields: field.fields,
newTableName,
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
})
}
case 'row':
case 'collapsible': {
return traverseFields({
...args,
fields: field.fields,
})
}
case 'array': { case 'array': {
const newTableName = args.adapter.tableNameMap.get( const newTableName = args.adapter.tableNameMap.get(
`${args.newTableName}_${toSnakeCase(field.name)}`, `${args.newTableName}_${toSnakeCase(field.name)}`,
@@ -82,7 +58,42 @@ export const traverseFields = (args: Args) => {
}) })
}) })
} }
case 'collapsible':
case 'row': {
return traverseFields({
...args,
fields: field.fields,
})
}
case 'group': {
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
if (field.localized && args.payload.config.localization) {
newTableName += args.adapter.localesSuffix
}
return traverseFields({
...args,
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
fields: field.fields,
newTableName,
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
})
}
case 'relationship':
case 'upload': {
if (typeof field.relationTo === 'string') {
if (field.type === 'upload' || !field.hasMany) {
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
}
}
return null
}
case 'tabs': { case 'tabs': {
return field.tabs.forEach((tab) => { return field.tabs.forEach((tab) => {
if (tabHasName(tab)) { if (tabHasName(tab)) {
@@ -101,17 +112,6 @@ export const traverseFields = (args: Args) => {
}) })
}) })
} }
case 'relationship':
case 'upload': {
if (typeof field.relationTo === 'string') {
if (field.type === 'upload' || !field.hasMany) {
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
}
}
return null
}
} }
}) })
} }

View File

@@ -179,183 +179,6 @@ export const traverseFields = ({
} }
switch (field.type) { switch (field.type) {
case 'text': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyTextField = true
}
if (field.index) {
hasManyTextField = 'index'
} else if (!hasManyTextField) {
hasManyTextField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in SQLite for hasMany text fields.',
)
}
} else {
targetTable[fieldName] = withDefault(text(columnName), field)
}
break
}
case 'email':
case 'code':
case 'textarea': {
targetTable[fieldName] = withDefault(text(columnName), field)
break
}
case 'number': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyNumberField = true
}
if (field.index) {
hasManyNumberField = 'index'
} else if (!hasManyNumberField) {
hasManyNumberField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany number fields.',
)
}
} else {
targetTable[fieldName] = withDefault(numeric(columnName), field)
}
break
}
case 'richText':
case 'json': {
targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
break
}
case 'date': {
targetTable[fieldName] = withDefault(text(columnName), field)
break
}
case 'point': {
break
}
case 'radio':
case 'select': {
const options = field.options.map((option) => {
if (optionIsObject(option)) {
return option.value
}
return option
}) as [string, ...string[]]
if (field.type === 'select' && field.hasMany) {
const selectTableName = createTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
versionsCustomName: versions,
})
const baseColumns: Record<string, SQLiteColumnBuilder> = {
order: integer('order').notNull(),
parent: getIDColumn({
name: 'parent_id',
type: parentIDColType,
notNull: true,
primaryKey: false,
}),
value: text('value', { enum: options }),
}
const baseExtraConfig: BaseExtraConfig = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns.locale = text('locale', { enum: locales }).notNull()
baseExtraConfig.localeIdx = (cols) =>
index(`${selectTableName}_locale_idx`).on(cols.locale)
}
if (field.index) {
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
}
buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull,
disableUnique,
fields: [],
rootTableName,
tableName: selectTableName,
versions,
})
relationsToBuild.set(fieldName, {
type: 'many',
// selects have their own localized table, independent of the base table.
localized: false,
target: selectTableName,
})
adapter.relations[`relations_${selectTableName}`] = relations(
adapter.tables[selectTableName],
({ one }) => ({
parent: one(adapter.tables[parentTableName], {
fields: [adapter.tables[selectTableName].parent],
references: [adapter.tables[parentTableName].id],
relationName: fieldName,
}),
}),
)
} else {
targetTable[fieldName] = withDefault(
text(columnName, {
enum: options,
}),
field,
)
}
break
}
case 'checkbox': {
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
break
}
case 'array': { case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -493,7 +316,6 @@ export const traverseFields = ({
break break
} }
case 'blocks': { case 'blocks': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -646,9 +468,82 @@ export const traverseFields = ({
break break
} }
case 'checkbox': {
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
break
}
case 'code':
case 'tab': case 'email':
case 'group': {
case 'textarea': {
targetTable[fieldName] = withDefault(text(columnName), field)
break
}
case 'collapsible':
case 'row': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
hasManyNumberField: rowHasManyNumberField,
hasManyTextField: rowHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.fields,
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
})
if (rowHasLocalizedField) {
hasLocalizedField = true
}
if (rowHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (rowHasManyTextField) {
hasManyTextField = true
}
if (rowHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (rowHasManyNumberField) {
hasManyNumberField = true
}
if (rowHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
}
break
}
case 'date': {
targetTable[fieldName] = withDefault(text(columnName), field)
break
}
case 'group':
case 'tab': {
if (!('name' in field)) { if (!('name' in field)) {
const { const {
hasLocalizedField: groupHasLocalizedField, hasLocalizedField: groupHasLocalizedField,
@@ -758,114 +653,136 @@ export const traverseFields = ({
break break
} }
case 'tabs': { case 'json':
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const { case 'richText': {
hasLocalizedField: tabHasLocalizedField, targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField, break
hasLocalizedManyTextField: tabHasLocalizedManyTextField, }
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyNumberField: tabHasManyNumberField,
hasManyTextField: tabHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
})
if (tabHasLocalizedField) { case 'number': {
hasLocalizedField = true if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyNumberField = true
} }
if (tabHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true if (field.index) {
} hasManyNumberField = 'index'
if (tabHasManyTextField) { } else if (!hasManyNumberField) {
hasManyTextField = true
}
if (tabHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (tabHasManyNumberField) {
hasManyNumberField = true hasManyNumberField = true
} }
if (tabHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany number fields.',
)
}
} else {
targetTable[fieldName] = withDefault(numeric(columnName), field)
} }
break break
} }
case 'row': case 'point': {
case 'collapsible': { break
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull }
const { case 'radio':
hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField, case 'select': {
hasLocalizedManyTextField: rowHasLocalizedManyTextField, const options = field.options.map((option) => {
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField, if (optionIsObject(option)) {
hasManyNumberField: rowHasManyNumberField, return option.value
hasManyTextField: rowHasManyTextField, }
} = traverseFields({
return option
}) as [string, ...string[]]
if (field.type === 'select' && field.hasMany) {
const selectTableName = createTableName({
adapter, adapter,
columnPrefix, config: field,
columns, parentTableName: newTableName,
disableNotNull: disableNotNullFromHere, prefix: `${newTableName}_`,
versionsCustomName: versions,
})
const baseColumns: Record<string, SQLiteColumnBuilder> = {
order: integer('order').notNull(),
parent: getIDColumn({
name: 'parent_id',
type: parentIDColType,
notNull: true,
primaryKey: false,
}),
value: text('value', { enum: options }),
}
const baseExtraConfig: BaseExtraConfig = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns.locale = text('locale', { enum: locales }).notNull()
baseExtraConfig.localeIdx = (cols) =>
index(`${selectTableName}_locale_idx`).on(cols.locale)
}
if (field.index) {
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
}
buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull,
disableUnique, disableUnique,
fieldPrefix, fields: [],
fields: field.fields,
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName, rootTableName,
uniqueRelationships, tableName: selectTableName,
versions, versions,
withinLocalizedArrayOrBlock,
}) })
if (rowHasLocalizedField) { relationsToBuild.set(fieldName, {
hasLocalizedField = true type: 'many',
} // selects have their own localized table, independent of the base table.
if (rowHasLocalizedRelationshipField) { localized: false,
hasLocalizedRelationshipField = true target: selectTableName,
} })
if (rowHasManyTextField) {
hasManyTextField = true adapter.relations[`relations_${selectTableName}`] = relations(
} adapter.tables[selectTableName],
if (rowHasLocalizedManyTextField) { ({ one }) => ({
hasLocalizedManyTextField = true parent: one(adapter.tables[parentTableName], {
} fields: [adapter.tables[selectTableName].parent],
if (rowHasManyNumberField) { references: [adapter.tables[parentTableName].id],
hasManyNumberField = true relationName: fieldName,
} }),
if (rowHasLocalizedManyNumberField) { }),
hasLocalizedManyNumberField = true )
} else {
targetTable[fieldName] = withDefault(
text(columnName, {
enum: options,
}),
field,
)
} }
break break
} }
@@ -931,6 +848,89 @@ export const traverseFields = ({
break break
case 'tabs': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: tabHasLocalizedField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyNumberField: tabHasManyNumberField,
hasManyTextField: tabHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
forceLocalized,
indexes,
locales,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
})
if (tabHasLocalizedField) {
hasLocalizedField = true
}
if (tabHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (tabHasManyTextField) {
hasManyTextField = true
}
if (tabHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (tabHasManyNumberField) {
hasManyNumberField = true
}
if (tabHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
}
break
}
case 'text': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyTextField = true
}
if (field.index) {
hasManyTextField = 'index'
} else if (!hasManyTextField) {
hasManyTextField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in SQLite for hasMany text fields.',
)
}
} else {
targetTable[fieldName] = withDefault(text(columnName), field)
}
break
}
default: default:
break break
} }

View File

@@ -13,44 +13,6 @@ type Args = {
export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => { export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
fields.forEach((field) => { fields.forEach((field) => {
switch (field.type) { switch (field.type) {
case 'group': {
const newPath = `${path ? `${path}.` : ''}${field.name}`
const newDoc = doc?.[field.name]
if (typeof newDoc === 'object' && newDoc !== null) {
if (field.localized) {
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
return traverseFields({
doc: localeDoc,
fields: field.fields,
locale,
path: newPath,
rows,
})
})
} else {
return traverseFields({
doc: newDoc as Record<string, unknown>,
fields: field.fields,
path: newPath,
rows,
})
}
}
break
}
case 'row':
case 'collapsible': {
return traverseFields({
doc,
fields: field.fields,
path,
rows,
})
}
case 'array': { case 'array': {
const rowData = doc?.[field.name] const rowData = doc?.[field.name]
@@ -124,19 +86,27 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
break break
} }
case 'collapsible':
// falls through
case 'row': {
return traverseFields({
doc,
fields: field.fields,
path,
rows,
})
}
case 'tabs': { case 'group': {
return field.tabs.forEach((tab) => { const newPath = `${path ? `${path}.` : ''}${field.name}`
if (tabHasName(tab)) { const newDoc = doc?.[field.name]
const newDoc = doc?.[tab.name]
const newPath = `${path ? `${path}.` : ''}${tab.name}`
if (typeof newDoc === 'object' && newDoc !== null) { if (typeof newDoc === 'object' && newDoc !== null) {
if (tab.localized) { if (field.localized) {
Object.entries(newDoc).forEach(([locale, localeDoc]) => { Object.entries(newDoc).forEach(([locale, localeDoc]) => {
return traverseFields({ return traverseFields({
doc: localeDoc, doc: localeDoc,
fields: tab.fields, fields: field.fields,
locale, locale,
path: newPath, path: newPath,
rows, rows,
@@ -145,24 +115,18 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
} else { } else {
return traverseFields({ return traverseFields({
doc: newDoc as Record<string, unknown>, doc: newDoc as Record<string, unknown>,
fields: tab.fields, fields: field.fields,
path: newPath, path: newPath,
rows, rows,
}) })
} }
} }
} else {
traverseFields({ break
doc,
fields: tab.fields,
path,
rows,
})
}
})
} }
case 'relationship': case 'relationship':
// falls through
case 'upload': { case 'upload': {
if (typeof field.relationTo === 'string') { if (typeof field.relationTo === 'string') {
if (field.type === 'upload' || !field.hasMany) { if (field.type === 'upload' || !field.hasMany) {
@@ -211,6 +175,43 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
} }
} }
} }
break
}
case 'tabs': {
return field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
const newDoc = doc?.[tab.name]
const newPath = `${path ? `${path}.` : ''}${tab.name}`
if (typeof newDoc === 'object' && newDoc !== null) {
if (tab.localized) {
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
return traverseFields({
doc: localeDoc,
fields: tab.fields,
locale,
path: newPath,
rows,
})
})
} else {
return traverseFields({
doc: newDoc as Record<string, unknown>,
fields: tab.fields,
path: newPath,
rows,
})
}
}
} else {
traverseFields({
doc,
fields: tab.fields,
path,
rows,
})
}
})
} }
} }
}) })

View File

@@ -27,30 +27,6 @@ type Args = {
export const traverseFields = (args: Args) => { export const traverseFields = (args: Args) => {
args.fields.forEach((field) => { args.fields.forEach((field) => {
switch (field.type) { switch (field.type) {
case 'group': {
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
if (field.localized && args.payload.config.localization) {
newTableName += args.adapter.localesSuffix
}
return traverseFields({
...args,
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
fields: field.fields,
newTableName,
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
})
}
case 'row':
case 'collapsible': {
return traverseFields({
...args,
fields: field.fields,
})
}
case 'array': { case 'array': {
const newTableName = args.adapter.tableNameMap.get( const newTableName = args.adapter.tableNameMap.get(
`${args.newTableName}_${toSnakeCase(field.name)}`, `${args.newTableName}_${toSnakeCase(field.name)}`,
@@ -82,7 +58,42 @@ export const traverseFields = (args: Args) => {
}) })
}) })
} }
case 'collapsible':
case 'row': {
return traverseFields({
...args,
fields: field.fields,
})
}
case 'group': {
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
if (field.localized && args.payload.config.localization) {
newTableName += args.adapter.localesSuffix
}
return traverseFields({
...args,
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
fields: field.fields,
newTableName,
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
})
}
case 'relationship':
case 'upload': {
if (typeof field.relationTo === 'string') {
if (field.type === 'upload' || !field.hasMany) {
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
}
}
return null
}
case 'tabs': { case 'tabs': {
return field.tabs.forEach((tab) => { return field.tabs.forEach((tab) => {
if (tabHasName(tab)) { if (tabHasName(tab)) {
@@ -101,17 +112,6 @@ export const traverseFields = (args: Args) => {
}) })
}) })
} }
case 'relationship':
case 'upload': {
if (typeof field.relationTo === 'string') {
if (field.type === 'upload' || !field.hasMany) {
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
}
}
return null
}
} }
}) })
} }

View File

@@ -218,32 +218,6 @@ export const traverseFields = ({
break break
} }
case 'select': {
if (field.hasMany) {
if (select) {
if (
(selectMode === 'include' && !select[field.name]) ||
(selectMode === 'exclude' && select[field.name] === false)
) {
break
}
}
const withSelect: Result = {
columns: {
id: false,
order: false,
parent: false,
},
orderBy: ({ order }, { asc }) => [asc(order)],
}
currentArgs.with[`${path}${field.name}`] = withSelect
}
break
}
case 'blocks': { case 'blocks': {
const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name] const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name]
@@ -356,6 +330,7 @@ export const traverseFields = ({
} }
case 'group': case 'group':
case 'tab': { case 'tab': {
const fieldSelect = select?.[field.name] const fieldSelect = select?.[field.name]
@@ -389,47 +364,6 @@ export const traverseFields = ({
break break
} }
case 'point': {
if (adapter.name === 'sqlite') {
break
}
const args = field.localized ? _locales : currentArgs
if (!args.columns) {
args.columns = {}
}
if (!args.extras) {
args.extras = {}
}
const name = `${path}${field.name}`
// Drizzle handles that poorly. See https://github.com/drizzle-team/drizzle-orm/issues/2526
// Additionally, this way we format the column value straight in the database using ST_AsGeoJSON
args.columns[name] = false
let shouldSelect = false
if (select || selectAllOnCurrentLevel) {
if (
selectAllOnCurrentLevel ||
(selectMode === 'include' && select[field.name] === true) ||
(selectMode === 'exclude' && typeof select[field.name] === 'undefined')
) {
shouldSelect = true
}
} else {
shouldSelect = true
}
if (shouldSelect) {
args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name)
}
break
}
case 'join': { case 'join': {
// when `joinsQuery` is false, do not join // when `joinsQuery` is false, do not join
if (joinQuery === false) { if (joinQuery === false) {
@@ -621,6 +555,72 @@ export const traverseFields = ({
break break
} }
case 'point': {
if (adapter.name === 'sqlite') {
break
}
const args = field.localized ? _locales : currentArgs
if (!args.columns) {
args.columns = {}
}
if (!args.extras) {
args.extras = {}
}
const name = `${path}${field.name}`
// Drizzle handles that poorly. See https://github.com/drizzle-team/drizzle-orm/issues/2526
// Additionally, this way we format the column value straight in the database using ST_AsGeoJSON
args.columns[name] = false
let shouldSelect = false
if (select || selectAllOnCurrentLevel) {
if (
selectAllOnCurrentLevel ||
(selectMode === 'include' && select[field.name] === true) ||
(selectMode === 'exclude' && typeof select[field.name] === 'undefined')
) {
shouldSelect = true
}
} else {
shouldSelect = true
}
if (shouldSelect) {
args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name)
}
break
}
case 'select': {
if (field.hasMany) {
if (select) {
if (
(selectMode === 'include' && !select[field.name]) ||
(selectMode === 'exclude' && select[field.name] === false)
) {
break
}
}
const withSelect: Result = {
columns: {
id: false,
order: false,
parent: false,
},
orderBy: ({ order }, { asc }) => [asc(order)],
}
currentArgs.with[`${path}${field.name}`] = withSelect
}
break
}
default: { default: {
if (!select && !selectAllOnCurrentLevel) { if (!select && !selectAllOnCurrentLevel) {
break break

View File

@@ -182,197 +182,6 @@ export const traverseFields = ({
} }
switch (field.type) { switch (field.type) {
case 'text': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyTextField = true
}
if (field.index) {
hasManyTextField = 'index'
} else if (!hasManyTextField) {
hasManyTextField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany text fields.',
)
}
} else {
targetTable[fieldName] = withDefault(varchar(columnName), field)
}
break
}
case 'email':
case 'code':
case 'textarea': {
targetTable[fieldName] = withDefault(varchar(columnName), field)
break
}
case 'number': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyNumberField = true
}
if (field.index) {
hasManyNumberField = 'index'
} else if (!hasManyNumberField) {
hasManyNumberField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany number fields.',
)
}
} else {
targetTable[fieldName] = withDefault(numeric(columnName), field)
}
break
}
case 'richText':
case 'json': {
targetTable[fieldName] = withDefault(jsonb(columnName), field)
break
}
case 'date': {
targetTable[fieldName] = withDefault(
timestamp(columnName, {
mode: 'string',
precision: 3,
withTimezone: true,
}),
field,
)
break
}
case 'point': {
targetTable[fieldName] = withDefault(geometryColumn(columnName), field)
if (!adapter.extensions.postgis) {
adapter.extensions.postgis = true
}
break
}
case 'radio':
case 'select': {
const enumName = createTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
})
adapter.enums[enumName] = adapter.pgSchema.enum(
enumName,
field.options.map((option) => {
if (optionIsObject(option)) {
return option.value
}
return option
}) as [string, ...string[]],
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = createTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versionsCustomName: versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
value: adapter.enums[enumName]('value'),
}
const baseExtraConfig: BaseExtraConfig = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
baseExtraConfig.localeIdx = (cols) =>
index(`${selectTableName}_locale_idx`).on(cols.locale)
}
if (field.index) {
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
}
buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull,
disableUnique,
fields: [],
rootTableName,
tableName: selectTableName,
versions,
})
relationsToBuild.set(fieldName, {
type: 'many',
// selects have their own localized table, independent of the base table.
localized: false,
target: selectTableName,
})
adapter.relations[`relations_${selectTableName}`] = relations(
adapter.tables[selectTableName],
({ one }) => ({
parent: one(adapter.tables[parentTableName], {
fields: [adapter.tables[selectTableName].parent],
references: [adapter.tables[parentTableName].id],
relationName: fieldName,
}),
}),
)
} else {
targetTable[fieldName] = withDefault(adapter.enums[enumName](columnName), field)
}
break
}
case 'checkbox': {
targetTable[fieldName] = withDefault(boolean(columnName), field)
break
}
case 'array': { case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -506,7 +315,6 @@ export const traverseFields = ({
break break
} }
case 'blocks': { case 'blocks': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -655,9 +463,88 @@ export const traverseFields = ({
break break
} }
case 'checkbox': {
targetTable[fieldName] = withDefault(boolean(columnName), field)
break
}
case 'code':
case 'tab': case 'email':
case 'group': {
case 'textarea': {
targetTable[fieldName] = withDefault(varchar(columnName), field)
break
}
case 'collapsible':
case 'row': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
hasManyNumberField: rowHasManyNumberField,
hasManyTextField: rowHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.fields,
forceLocalized,
indexes,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
})
if (rowHasLocalizedField) {
hasLocalizedField = true
}
if (rowHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (rowHasManyTextField) {
hasManyTextField = true
}
if (rowHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (rowHasManyNumberField) {
hasManyNumberField = true
}
if (rowHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
}
break
}
case 'date': {
targetTable[fieldName] = withDefault(
timestamp(columnName, {
mode: 'string',
precision: 3,
withTimezone: true,
}),
field,
)
break
}
case 'group':
case 'tab': {
if (!('name' in field)) { if (!('name' in field)) {
const { const {
hasLocalizedField: groupHasLocalizedField, hasLocalizedField: groupHasLocalizedField,
@@ -765,112 +652,143 @@ export const traverseFields = ({
break break
} }
case 'tabs': { case 'json':
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const { case 'richText': {
hasLocalizedField: tabHasLocalizedField, targetTable[fieldName] = withDefault(jsonb(columnName), field)
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField, break
hasLocalizedManyTextField: tabHasLocalizedManyTextField, }
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyNumberField: tabHasManyNumberField,
hasManyTextField: tabHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
forceLocalized,
indexes,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
})
if (tabHasLocalizedField) { case 'number': {
hasLocalizedField = true if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyNumberField = true
} }
if (tabHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true if (field.index) {
} hasManyNumberField = 'index'
if (tabHasManyTextField) { } else if (!hasManyNumberField) {
hasManyTextField = true
}
if (tabHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (tabHasManyNumberField) {
hasManyNumberField = true hasManyNumberField = true
} }
if (tabHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany number fields.',
)
}
} else {
targetTable[fieldName] = withDefault(numeric(columnName), field)
} }
break break
} }
case 'row': case 'point': {
case 'collapsible': { targetTable[fieldName] = withDefault(geometryColumn(columnName), field)
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull if (!adapter.extensions.postgis) {
const { adapter.extensions.postgis = true
hasLocalizedField: rowHasLocalizedField, }
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField, break
hasLocalizedManyTextField: rowHasLocalizedManyTextField, }
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField, case 'radio':
hasManyNumberField: rowHasManyNumberField,
hasManyTextField: rowHasManyTextField, case 'select': {
} = traverseFields({ const enumName = createTableName({
adapter, adapter,
columnPrefix, config: field,
columns, parentTableName: newTableName,
disableNotNull: disableNotNullFromHere, prefix: `enum_${newTableName}_`,
disableUnique, target: 'enumName',
fieldPrefix, throwValidationError,
fields: field.fields,
forceLocalized,
indexes,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
}) })
if (rowHasLocalizedField) { adapter.enums[enumName] = adapter.pgSchema.enum(
hasLocalizedField = true enumName,
field.options.map((option) => {
if (optionIsObject(option)) {
return option.value
} }
if (rowHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true return option
}) as [string, ...string[]],
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = createTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versionsCustomName: versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
value: adapter.enums[enumName]('value'),
} }
if (rowHasManyTextField) {
hasManyTextField = true const baseExtraConfig: BaseExtraConfig = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
} }
if (rowHasLocalizedManyTextField) {
hasLocalizedManyTextField = true const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
baseExtraConfig.localeIdx = (cols) =>
index(`${selectTableName}_locale_idx`).on(cols.locale)
} }
if (rowHasManyNumberField) {
hasManyNumberField = true if (field.index) {
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
} }
if (rowHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull,
disableUnique,
fields: [],
rootTableName,
tableName: selectTableName,
versions,
})
relationsToBuild.set(fieldName, {
type: 'many',
// selects have their own localized table, independent of the base table.
localized: false,
target: selectTableName,
})
adapter.relations[`relations_${selectTableName}`] = relations(
adapter.tables[selectTableName],
({ one }) => ({
parent: one(adapter.tables[parentTableName], {
fields: [adapter.tables[selectTableName].parent],
references: [adapter.tables[parentTableName].id],
relationName: fieldName,
}),
}),
)
} else {
targetTable[fieldName] = withDefault(adapter.enums[enumName](columnName), field)
} }
break break
} }
@@ -936,6 +854,88 @@ export const traverseFields = ({
break break
case 'tabs': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: tabHasLocalizedField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyNumberField: tabHasManyNumberField,
hasManyTextField: tabHasManyTextField,
} = traverseFields({
adapter,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
forceLocalized,
indexes,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
})
if (tabHasLocalizedField) {
hasLocalizedField = true
}
if (tabHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (tabHasManyTextField) {
hasManyTextField = true
}
if (tabHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (tabHasManyNumberField) {
hasManyNumberField = true
}
if (tabHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
}
break
}
case 'text': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
if (isLocalized) {
hasLocalizedManyTextField = true
}
if (field.index) {
hasManyTextField = 'index'
} else if (!hasManyTextField) {
hasManyTextField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany text fields.',
)
}
} else {
targetTable[fieldName] = withDefault(varchar(columnName), field)
}
break
}
default: default:
break break
} }

View File

@@ -121,185 +121,6 @@ export const getTableColumnFromPath = ({
} }
switch (field.type) { switch (field.type) {
case 'tabs': {
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix,
constraintPath,
constraints,
fields: field.tabs.map((tab) => ({
...tab,
type: 'tab',
})),
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix,
value,
})
}
case 'tab': {
if (tabHasName(field)) {
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath: `${constraintPath}${field.name}.`,
constraints,
fields: field.fields,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
value,
})
}
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix,
constraintPath,
constraints,
fields: field.fields,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix,
value,
})
}
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}${adapter.localesSuffix}`
let condition = eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID)
if (locale !== 'all') {
condition = and(condition, eq(adapter.tables[newTableName]._locale, locale))
}
addJoinTable({
condition,
joins,
table: adapter.tables[newTableName],
})
}
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath: `${constraintPath}${field.name}.`,
constraints,
fields: field.fields,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
value,
})
}
case 'select': {
if (field.hasMany) {
const newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName]._locale, locale),
]
if (locale !== 'all') {
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
}
addJoinTable({
condition: and(...conditions),
joins,
table: adapter.tables[newTableName],
})
} else {
addJoinTable({
condition: eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
joins,
table: adapter.tables[newTableName],
})
}
return {
columnName: 'value',
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'text':
case 'number': {
if (field.hasMany) {
let tableType = 'texts'
let columnName = 'text'
if (field.type === 'number') {
tableType = 'numbers'
columnName = 'number'
}
newTableName = `${rootTableName}_${tableType}`
const joinConstraints = [
eq(adapter.tables[rootTableName].id, adapter.tables[newTableName].parent),
like(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [...joinConstraints]
if (locale !== 'all') {
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
}
addJoinTable({
condition: and(...conditions),
joins,
table: adapter.tables[newTableName],
})
} else {
addJoinTable({
condition: and(...joinConstraints),
joins,
table: adapter.tables[newTableName],
})
}
return {
columnName,
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'array': { case 'array': {
newTableName = adapter.tableNameMap.get( newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`, `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
@@ -341,7 +162,6 @@ export const getTableColumnFromPath = ({
value, value,
}) })
} }
case 'blocks': { case 'blocks': {
let blockTableColumn: TableColumn let blockTableColumn: TableColumn
let newTableName: string let newTableName: string
@@ -447,7 +267,87 @@ export const getTableColumnFromPath = ({
break break
} }
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}${adapter.localesSuffix}`
let condition = eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID)
if (locale !== 'all') {
condition = and(condition, eq(adapter.tables[newTableName]._locale, locale))
}
addJoinTable({
condition,
joins,
table: adapter.tables[newTableName],
})
}
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath: `${constraintPath}${field.name}.`,
constraints,
fields: field.fields,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
value,
})
}
case 'number':
case 'text': {
if (field.hasMany) {
let tableType = 'texts'
let columnName = 'text'
if (field.type === 'number') {
tableType = 'numbers'
columnName = 'number'
}
newTableName = `${rootTableName}_${tableType}`
const joinConstraints = [
eq(adapter.tables[rootTableName].id, adapter.tables[newTableName].parent),
like(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [...joinConstraints]
if (locale !== 'all') {
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
}
addJoinTable({
condition: and(...conditions),
joins,
table: adapter.tables[newTableName],
})
} else {
addJoinTable({
condition: and(...joinConstraints),
joins,
table: adapter.tables[newTableName],
})
}
return {
columnName,
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'relationship': case 'relationship':
case 'upload': { case 'upload': {
const newCollectionPath = pathSegments.slice(1).join('.') const newCollectionPath = pathSegments.slice(1).join('.')
if (Array.isArray(field.relationTo) || field.hasMany) { if (Array.isArray(field.relationTo) || field.hasMany) {
@@ -692,6 +592,106 @@ export const getTableColumnFromPath = ({
break break
} }
case 'select': {
if (field.hasMany) {
const newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
if (locale && field.localized && adapter.payload.config.localization) {
const conditions = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName]._locale, locale),
]
if (locale !== 'all') {
conditions.push(eq(adapter.tables[newTableName]._locale, locale))
}
addJoinTable({
condition: and(...conditions),
joins,
table: adapter.tables[newTableName],
})
} else {
addJoinTable({
condition: eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
joins,
table: adapter.tables[newTableName],
})
}
return {
columnName: 'value',
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'tab': {
if (tabHasName(field)) {
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath: `${constraintPath}${field.name}.`,
constraints,
fields: field.fields,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
value,
})
}
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix,
constraintPath,
constraints,
fields: field.fields,
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix,
value,
})
}
case 'tabs': {
return getTableColumnFromPath({
adapter,
aliasTable,
collectionPath,
columnPrefix,
constraintPath,
constraints,
fields: field.tabs.map((tab) => ({
...tab,
type: 'tab',
})),
joins,
locale,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
tableName: newTableName,
tableNameSuffix,
value,
})
}
default: { default: {
// fall through // fall through
break break

View File

@@ -295,6 +295,13 @@ export function parseParams({
if (field.type === 'point' && adapter.name === 'postgres') { if (field.type === 'point' && adapter.name === 'postgres') {
switch (operator) { switch (operator) {
case 'intersects': {
constraints.push(
sql`ST_Intersects(${table[columnName]}, ST_GeomFromGeoJSON(${JSON.stringify(queryValue)}))`,
)
break
}
case 'near': { case 'near': {
const [lng, lat, maxDistance, minDistance] = queryValue as number[] const [lng, lat, maxDistance, minDistance] = queryValue as number[]
@@ -313,13 +320,6 @@ export function parseParams({
break break
} }
case 'intersects': {
constraints.push(
sql`ST_Intersects(${table[columnName]}, ST_GeomFromGeoJSON(${JSON.stringify(queryValue)}))`,
)
break
}
default: default:
break break
} }

View File

@@ -593,8 +593,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
let val = fieldData let val = fieldData
switch (field.type) { switch (field.type) {
case 'tab': case 'date': {
case 'group': { if (typeof fieldData === 'string') {
val = new Date(fieldData).toISOString()
}
break
}
case 'group':
case 'tab': {
const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_` const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_`
const groupData = {} const groupData = {}
const locale = table._locale as string const locale = table._locale as string
@@ -626,14 +634,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
return return
} }
case 'text': {
if (typeof fieldData === 'string') {
val = String(fieldData)
}
break
}
case 'number': { case 'number': {
if (typeof fieldData === 'string') { if (typeof fieldData === 'string') {
val = Number.parseFloat(fieldData) val = Number.parseFloat(fieldData)
@@ -642,15 +642,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
break break
} }
case 'date': {
if (typeof fieldData === 'string') {
val = new Date(fieldData).toISOString()
}
break
}
case 'relationship': case 'relationship':
case 'upload': { case 'upload': {
if ( if (
val && val &&
@@ -662,6 +655,13 @@ export const traverseFields = <T extends Record<string, unknown>>({
break break
} }
case 'text': {
if (typeof fieldData === 'string') {
val = String(fieldData)
}
break
}
default: { default: {
break break

View File

@@ -17,23 +17,23 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@eslint-react/eslint-plugin": "1.12.3", "@eslint-react/eslint-plugin": "1.16.1",
"@eslint/js": "9.9.1", "@eslint/js": "9.14.0",
"@payloadcms/eslint-plugin": "workspace:*", "@payloadcms/eslint-plugin": "workspace:*",
"@types/eslint": "9.6.1", "@types/eslint": "9.6.1",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@typescript-eslint/parser": "8.3.0", "@typescript-eslint/parser": "8.14.0",
"eslint": "9.9.1", "eslint": "9.14.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-import-x": "4.1.1", "eslint-plugin-import-x": "4.4.2",
"eslint-plugin-jest": "28.8.1", "eslint-plugin-jest": "28.9.0",
"eslint-plugin-jest-dom": "5.4.0", "eslint-plugin-jest-dom": "5.4.0",
"eslint-plugin-jsx-a11y": "6.9.0", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.3.0", "eslint-plugin-perfectionist": "3.9.1",
"eslint-plugin-react-hooks": "5.1.0-rc-a19a8ab4-20240829", "eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-regexp": "2.6.0", "eslint-plugin-regexp": "2.6.0",
"globals": "15.9.0", "globals": "15.12.0",
"typescript": "5.6.3", "typescript": "5.6.3",
"typescript-eslint": "8.3.0" "typescript-eslint": "8.14.0"
} }
} }

View File

@@ -17,22 +17,22 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@eslint-react/eslint-plugin": "1.12.3", "@eslint-react/eslint-plugin": "1.16.1",
"@eslint/js": "9.9.1", "@eslint/js": "9.14.0",
"@types/eslint": "9.6.1", "@types/eslint": "9.6.1",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@typescript-eslint/parser": "8.3.0", "@typescript-eslint/parser": "8.14.0",
"eslint": "9.9.1", "eslint": "9.14.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-import-x": "4.1.1", "eslint-plugin-import-x": "4.4.2",
"eslint-plugin-jest": "28.8.1", "eslint-plugin-jest": "28.9.0",
"eslint-plugin-jest-dom": "5.4.0", "eslint-plugin-jest-dom": "5.4.0",
"eslint-plugin-jsx-a11y": "6.9.0", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.3.0", "eslint-plugin-perfectionist": "3.9.1",
"eslint-plugin-react-hooks": "5.1.0-rc-a19a8ab4-20240829", "eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-regexp": "2.6.0", "eslint-plugin-regexp": "2.6.0",
"globals": "15.9.0", "globals": "15.12.0",
"typescript": "5.6.3", "typescript": "5.6.3",
"typescript-eslint": "8.3.0" "typescript-eslint": "8.14.0"
} }
} }

View File

@@ -394,15 +394,15 @@ export class QueryComplexity {
this.variableValues = coerced this.variableValues = coerced
switch (operation.operation) { switch (operation.operation) {
case 'query':
this.complexity += this.nodeComplexity(operation, this.context.getSchema().getQueryType())
break
case 'mutation': case 'mutation':
this.complexity += this.nodeComplexity( this.complexity += this.nodeComplexity(
operation, operation,
this.context.getSchema().getMutationType(), this.context.getSchema().getMutationType(),
) )
break break
case 'query':
this.complexity += this.nodeComplexity(operation, this.context.getSchema().getQueryType())
break
case 'subscription': case 'subscription':
this.complexity += this.nodeComplexity( this.complexity += this.nodeComplexity(
operation, operation,

View File

@@ -24,18 +24,18 @@ function parseObject(typeName, ast, variables) {
function parseLiteral(typeName, ast, variables) { function parseLiteral(typeName, ast, variables) {
switch (ast.kind) { switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN: case Kind.BOOLEAN:
case Kind.STRING:
return ast.value return ast.value
case Kind.INT:
case Kind.FLOAT: case Kind.FLOAT:
case Kind.INT:
return parseFloat(ast.value) return parseFloat(ast.value)
case Kind.OBJECT:
return parseObject(typeName, ast, variables)
case Kind.LIST: case Kind.LIST:
return ast.values.map((n) => parseLiteral(typeName, n, variables)) return ast.values.map((n) => parseLiteral(typeName, n, variables))
case Kind.NULL: case Kind.NULL:
return null return null
case Kind.OBJECT:
return parseObject(typeName, ast, variables)
case Kind.VARIABLE: case Kind.VARIABLE:
return variables ? variables[ast.name.value] : undefined return variables ? variables[ast.name.value] : undefined
default: default:

View File

@@ -24,16 +24,6 @@ export const traverseFields = <T>(args: {
const fieldName = fieldSchema.name const fieldName = fieldSchema.name
switch (fieldSchema.type) { switch (fieldSchema.type) {
case 'richText':
result[fieldName] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[fieldName],
populationsByCollection,
result: result[fieldName],
})
break
case 'array': case 'array':
if (Array.isArray(incomingData[fieldName])) { if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((incomingRow, i) => { result[fieldName] = incomingData[fieldName].map((incomingRow, i) => {
@@ -94,8 +84,9 @@ export const traverseFields = <T>(args: {
break break
case 'tabs':
case 'group': case 'group':
case 'tabs':
if (!result[fieldName]) { if (!result[fieldName]) {
result[fieldName] = {} result[fieldName] = {}
} }
@@ -109,9 +100,9 @@ export const traverseFields = <T>(args: {
}) })
break break
case 'relationship':
case 'upload': case 'upload':
case 'relationship':
// Handle `hasMany` relationships // Handle `hasMany` relationships
if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) { if (fieldSchema.hasMany && Array.isArray(incomingData[fieldName])) {
if (!result[fieldName] || !incomingData[fieldName].length) { if (!result[fieldName] || !incomingData[fieldName].length) {
@@ -271,6 +262,15 @@ export const traverseFields = <T>(args: {
} }
} }
break
case 'richText':
result[fieldName] = traverseRichText({
externallyUpdatedRelationship,
incomingData: incomingData[fieldName],
populationsByCollection,
result: result[fieldName],
})
break break
default: default:

View File

@@ -106,7 +106,7 @@
"babel-plugin-react-compiler": "0.0.0-experimental-24ec0eb-20240918", "babel-plugin-react-compiler": "0.0.0-experimental-24ec0eb-20240918",
"esbuild": "0.23.1", "esbuild": "0.23.1",
"esbuild-sass-plugin": "3.3.1", "esbuild-sass-plugin": "3.3.1",
"eslint-plugin-react-compiler": "0.0.0-experimental-7670337-20240918", "eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"payload": "workspace:*", "payload": "workspace:*",
"swc-plugin-transform-remove-imports": "1.15.0" "swc-plugin-transform-remove-imports": "1.15.0"
}, },

View File

@@ -1,5 +1,5 @@
const ACCEPTABLE_CONTENT_TYPE = /multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i const ACCEPTABLE_CONTENT_TYPE = /multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i
const UNACCEPTABLE_METHODS = new Set(['GET', 'HEAD', 'DELETE', 'OPTIONS', 'CONNECT', 'TRACE']) const UNACCEPTABLE_METHODS = new Set(['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'TRACE'])
const hasBody = (req: Request): boolean => { const hasBody = (req: Request): boolean => {
return Boolean( return Boolean(

View File

@@ -29,10 +29,10 @@ export type SizeReducerAction =
export const sizeReducer = (state: SizeReducerState, action: SizeReducerAction) => { export const sizeReducer = (state: SizeReducerState, action: SizeReducerAction) => {
switch (action.type) { switch (action.type) {
case 'width':
return { ...state, width: action.value }
case 'height': case 'height':
return { ...state, height: action.value } return { ...state, height: action.value }
case 'width':
return { ...state, width: action.value }
default: default:
return { ...state, ...(action?.value || {}) } return { ...state, ...(action?.value || {}) }
} }

View File

@@ -22,8 +22,8 @@ import type {
} from '../types.js' } from '../types.js'
export type ClientTab = export type ClientTab =
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
| ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>) | ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>)
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
type TabsFieldBaseClientProps = {} & Pick<ServerFieldBase, 'permissions'> type TabsFieldBaseClientProps = {} & Pick<ServerFieldBase, 'permissions'>

View File

@@ -28,9 +28,9 @@ export type AdminViewProps = {
readonly initialData?: Data readonly initialData?: Data
readonly initPageResult: InitPageResult readonly initPageResult: InitPageResult
readonly params?: { [key: string]: string | string[] | undefined } readonly params?: { [key: string]: string | string[] | undefined }
readonly searchParams: { [key: string]: string | string[] | undefined }
readonly redirectAfterDelete?: boolean readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean readonly redirectAfterDuplicate?: boolean
readonly searchParams: { [key: string]: string | string[] | undefined }
} }
export type AdminViewComponent = PayloadComponent<AdminViewProps> export type AdminViewComponent = PayloadComponent<AdminViewProps>

View File

@@ -17,8 +17,8 @@ const traverseFields = ({
}: TraverseFieldsArgs) => { }: TraverseFieldsArgs) => {
fields.forEach((field) => { fields.forEach((field) => {
switch (field.type) { switch (field.type) {
case 'row': case 'collapsible':
case 'collapsible': { case 'row': {
traverseFields({ traverseFields({
data, data,
fields: field.fields, fields: field.fields,
@@ -47,14 +47,6 @@ const traverseFields = ({
}) })
break break
} }
case 'tabs': {
traverseFields({
data,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
result,
})
break
}
case 'tab': { case 'tab': {
if (tabHasName(field)) { if (tabHasName(field)) {
let targetResult let targetResult
@@ -84,6 +76,14 @@ const traverseFields = ({
} }
break break
} }
case 'tabs': {
traverseFields({
data,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
result,
})
break
}
default: default:
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
if (field.saveToJWT) { if (field.saveToJWT) {

View File

@@ -65,21 +65,6 @@ export const migrate = async ({ config, parsedArgs }: Args): Promise<void> => {
case 'migrate': case 'migrate':
await adapter.migrate() await adapter.migrate()
break break
case 'migrate:status':
await adapter.migrateStatus()
break
case 'migrate:down':
await adapter.migrateDown()
break
case 'migrate:refresh':
await adapter.migrateRefresh()
break
case 'migrate:reset':
await adapter.migrateReset()
break
case 'migrate:fresh':
await adapter.migrateFresh({ forceAcceptWarning })
break
case 'migrate:create': case 'migrate:create':
try { try {
await adapter.createMigration({ await adapter.createMigration({
@@ -92,6 +77,21 @@ export const migrate = async ({ config, parsedArgs }: Args): Promise<void> => {
throw new Error(`Error creating migration: ${err.message}`) throw new Error(`Error creating migration: ${err.message}`)
} }
break break
case 'migrate:down':
await adapter.migrateDown()
break
case 'migrate:fresh':
await adapter.migrateFresh({ forceAcceptWarning })
break
case 'migrate:refresh':
await adapter.migrateRefresh()
break
case 'migrate:reset':
await adapter.migrateReset()
break
case 'migrate:status':
await adapter.migrateStatus()
break
default: default:
payload.logger.error({ payload.logger.error({

View File

@@ -92,8 +92,8 @@ export async function getLocalizedPaths({
switch (matchedField.type) { switch (matchedField.type) {
case 'blocks': case 'blocks':
case 'richText': case 'json':
case 'json': { case 'richText': {
const upcomingSegments = pathSegments.slice(i + 1).join('.') const upcomingSegments = pathSegments.slice(i + 1).join('.')
lastIncompletePath.complete = true lastIncompletePath.complete = true
lastIncompletePath.path = upcomingSegments lastIncompletePath.path = upcomingSegments

View File

@@ -111,8 +111,8 @@ export const createClientField = ({
switch (incomingField.type) { switch (incomingField.type) {
case 'array': case 'array':
case 'group':
case 'collapsible': case 'collapsible':
case 'group':
case 'row': { case 'row': {
const field = clientField as unknown as RowFieldClient const field = clientField as unknown as RowFieldClient
@@ -182,6 +182,31 @@ export const createClientField = ({
break break
} }
case 'radio':
case 'select': {
const field = clientField as RadioFieldClient | SelectFieldClient
if (incomingField.options?.length) {
for (let i = 0; i < incomingField.options.length; i++) {
const option = incomingField.options[i]
if (typeof option === 'object' && typeof option.label === 'function') {
if (!field.options) {
field.options = []
}
field.options[i] = {
label: option.label({ t: i18n.t }),
value: option.value,
}
}
}
}
break
}
case 'richText': { case 'richText': {
if (!incomingField?.editor) { if (!incomingField?.editor) {
throw new MissingEditorProp(incomingField) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor throw new MissingEditorProp(incomingField) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
@@ -193,7 +218,6 @@ export const createClientField = ({
break break
} }
case 'tabs': { case 'tabs': {
const field = clientField as unknown as TabsFieldClient const field = clientField as unknown as TabsFieldClient
@@ -221,30 +245,6 @@ export const createClientField = ({
break break
} }
case 'select':
case 'radio': {
const field = clientField as RadioFieldClient | SelectFieldClient
if (incomingField.options?.length) {
for (let i = 0; i < incomingField.options.length; i++) {
const option = incomingField.options[i]
if (typeof option === 'object' && typeof option.label === 'function') {
if (!field.options) {
field.options = []
}
field.options[i] = {
label: option.label({ t: i18n.t }),
value: option.value,
}
}
}
}
break
}
default: default:
break break
} }

View File

@@ -97,27 +97,6 @@ export const promise = async ({
// Traverse subfields // Traverse subfields
switch (field.type) { switch (field.type) {
case 'group': {
await traverseFields({
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
path: fieldPath,
previousDoc,
previousSiblingDoc: previousDoc[field.name] as JsonObject,
req,
schemaPath: fieldSchemaPath,
siblingData: (siblingData?.[field.name] as JsonObject) || {},
siblingDoc: siblingDoc[field.name] as JsonObject,
})
break
}
case 'array': { case 'array': {
const rows = siblingDoc[field.name] const rows = siblingDoc[field.name]
@@ -185,8 +164,9 @@ export const promise = async ({
break break
} }
case 'row': case 'collapsible':
case 'collapsible': {
case 'row': {
await traverseFields({ await traverseFields({
collection, collection,
context, context,
@@ -206,6 +186,66 @@ export const promise = async ({
break break
} }
case 'group': {
await traverseFields({
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
path: fieldPath,
previousDoc,
previousSiblingDoc: previousDoc[field.name] as JsonObject,
req,
schemaPath: fieldSchemaPath,
siblingData: (siblingData?.[field.name] as JsonObject) || {},
siblingDoc: siblingDoc[field.name] as JsonObject,
})
break
}
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.afterChange?.length) {
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
path: fieldPath,
previousDoc,
previousSiblingDoc,
previousValue: previousDoc[field.name],
req,
schemaPath: fieldSchemaPath,
siblingData,
value: siblingDoc[field.name],
})
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue
}
}, Promise.resolve())
}
break
}
case 'tab': { case 'tab': {
let tabSiblingData = siblingData let tabSiblingData = siblingData
@@ -258,46 +298,6 @@ export const promise = async ({
break break
} }
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.afterChange?.length) {
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
path: fieldPath,
previousDoc,
previousSiblingDoc,
previousValue: previousDoc[field.name],
req,
schemaPath: fieldSchemaPath,
siblingData,
value: siblingDoc[field.name],
})
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue
}
}, Promise.resolve())
}
break
}
default: { default: {
break break
} }

View File

@@ -181,15 +181,13 @@ export const promise = async ({
break break
} }
case 'tabs': { case 'point': {
field.tabs.forEach((tab) => { const pointDoc = siblingDoc[field.name] as Record<string, unknown>
if ( if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
tabHasName(tab) && siblingDoc[field.name] = pointDoc.coordinates
(typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null) } else {
) { siblingDoc[field.name] = undefined
siblingDoc[tab.name] = {}
} }
})
break break
} }
@@ -206,13 +204,15 @@ export const promise = async ({
break break
} }
case 'point': { case 'tabs': {
const pointDoc = siblingDoc[field.name] as Record<string, unknown> field.tabs.forEach((tab) => {
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) { if (
siblingDoc[field.name] = pointDoc.coordinates tabHasName(tab) &&
} else { (typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null)
siblingDoc[field.name] = undefined ) {
siblingDoc[tab.name] = {}
} }
})
break break
} }
@@ -347,45 +347,6 @@ export const promise = async ({
} }
switch (field.type) { switch (field.type) {
case 'group': {
let groupDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') {
groupDoc = {}
}
const groupSelect = select?.[field.name]
traverseFields({
collection,
context,
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
path: fieldPath,
populate,
populationPromises,
req,
schemaPath: fieldSchemaPath,
select: typeof groupSelect === 'object' ? groupSelect : undefined,
selectMode,
showHiddenFields,
siblingDoc: groupDoc,
triggerAccessControl,
triggerHooks,
})
break
}
case 'array': { case 'array': {
const rows = siblingDoc[field.name] as JsonObject const rows = siblingDoc[field.name] as JsonObject
@@ -573,8 +534,9 @@ export const promise = async ({
break break
} }
case 'row': case 'collapsible':
case 'collapsible': {
case 'row': {
traverseFields({ traverseFields({
collection, collection,
context, context,
@@ -605,22 +567,13 @@ export const promise = async ({
break break
} }
case 'group': {
case 'tab': { let groupDoc = siblingDoc[field.name] as JsonObject
let tabDoc = siblingDoc
let tabSelect: SelectType | undefined
if (tabHasName(field)) {
tabDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') { if (typeof siblingDoc[field.name] !== 'object') {
tabDoc = {} groupDoc = {}
} }
if (typeof select?.[field.name] === 'object') { const groupSelect = select?.[field.name]
tabSelect = select?.[field.name] as SelectType
}
} else {
tabSelect = select
}
traverseFields({ traverseFields({
collection, collection,
@@ -642,10 +595,10 @@ export const promise = async ({
populationPromises, populationPromises,
req, req,
schemaPath: fieldSchemaPath, schemaPath: fieldSchemaPath,
select: tabSelect, select: typeof groupSelect === 'object' ? groupSelect : undefined,
selectMode, selectMode,
showHiddenFields, showHiddenFields,
siblingDoc: tabDoc, siblingDoc: groupDoc,
triggerAccessControl, triggerAccessControl,
triggerHooks, triggerHooks,
}) })
@@ -653,37 +606,6 @@ export const promise = async ({
break break
} }
case 'tabs': {
traverseFields({
collection,
context,
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
findMany,
flattenLocales,
global,
locale,
overrideAccess,
path: fieldPath,
populate,
populationPromises,
req,
schemaPath: fieldSchemaPath,
select,
selectMode,
showHiddenFields,
siblingDoc,
triggerAccessControl,
triggerHooks,
})
break
}
case 'richText': { case 'richText': {
if (!field?.editor) { if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
@@ -781,6 +703,84 @@ export const promise = async ({
break break
} }
case 'tab': {
let tabDoc = siblingDoc
let tabSelect: SelectType | undefined
if (tabHasName(field)) {
tabDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') {
tabDoc = {}
}
if (typeof select?.[field.name] === 'object') {
tabSelect = select?.[field.name] as SelectType
}
} else {
tabSelect = select
}
traverseFields({
collection,
context,
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
global,
locale,
overrideAccess,
path: fieldPath,
populate,
populationPromises,
req,
schemaPath: fieldSchemaPath,
select: tabSelect,
selectMode,
showHiddenFields,
siblingDoc: tabDoc,
triggerAccessControl,
triggerHooks,
})
break
}
case 'tabs': {
traverseFields({
collection,
context,
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
findMany,
flattenLocales,
global,
locale,
overrideAccess,
path: fieldPath,
populate,
populationPromises,
req,
schemaPath: fieldSchemaPath,
select,
selectMode,
showHiddenFields,
siblingDoc,
triggerAccessControl,
triggerHooks,
})
break
}
default: { default: {
break break
} }

View File

@@ -2,9 +2,9 @@ import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js' import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { ValidationFieldError } from '../../../errors/index.js' import type { ValidationFieldError } from '../../../errors/index.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js' import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { RequestContext } from '../../../index.js'
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js' import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js' import type { Field, TabAsField } from '../../config/types.js'
import type { RequestContext } from '../../../index.js'
import { MissingEditorProp } from '../../../errors/index.js' import { MissingEditorProp } from '../../../errors/index.js'
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js' import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
@@ -200,60 +200,6 @@ export const promise = async ({
} }
switch (field.type) { switch (field.type) {
case 'point': {
// Transform point data for storage
if (
Array.isArray(siblingData[field.name]) &&
siblingData[field.name][0] !== null &&
siblingData[field.name][1] !== null
) {
siblingData[field.name] = {
type: 'Point',
coordinates: [
parseFloat(siblingData[field.name][0]),
parseFloat(siblingData[field.name][1]),
],
}
}
break
}
case 'group': {
if (typeof siblingData[field.name] !== 'object') {
siblingData[field.name] = {}
}
if (typeof siblingDoc[field.name] !== 'object') {
siblingDoc[field.name] = {}
}
if (typeof siblingDocWithLocales[field.name] !== 'object') {
siblingDocWithLocales[field.name] = {}
}
await traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: field.fields,
global,
mergeLocaleActions,
operation,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData[field.name] as JsonObject,
siblingDoc: siblingDoc[field.name] as JsonObject,
siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
skipValidation: skipValidationFromHere,
})
break
}
case 'array': { case 'array': {
const rows = siblingData[field.name] const rows = siblingData[field.name]
@@ -339,8 +285,9 @@ export const promise = async ({
break break
} }
case 'row': case 'collapsible':
case 'collapsible': {
case 'row': {
await traverseFields({ await traverseFields({
id, id,
collection, collection,
@@ -365,6 +312,104 @@ export const promise = async ({
break break
} }
case 'group': {
if (typeof siblingData[field.name] !== 'object') {
siblingData[field.name] = {}
}
if (typeof siblingDoc[field.name] !== 'object') {
siblingDoc[field.name] = {}
}
if (typeof siblingDocWithLocales[field.name] !== 'object') {
siblingDocWithLocales[field.name] = {}
}
await traverseFields({
id,
collection,
context,
data,
doc,
docWithLocales,
errors,
fields: field.fields,
global,
mergeLocaleActions,
operation,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData[field.name] as JsonObject,
siblingDoc: siblingDoc[field.name] as JsonObject,
siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
skipValidation: skipValidationFromHere,
})
break
}
case 'point': {
// Transform point data for storage
if (
Array.isArray(siblingData[field.name]) &&
siblingData[field.name][0] !== null &&
siblingData[field.name][1] !== null
) {
siblingData[field.name] = {
type: 'Point',
coordinates: [
parseFloat(siblingData[field.name][0]),
parseFloat(siblingData[field.name][1]),
],
}
}
break
}
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeChange?.length) {
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
docWithLocales,
errors,
field,
global,
mergeLocaleActions,
operation,
originalDoc: doc,
path: fieldPath,
previousSiblingDoc: siblingDoc,
previousValue: siblingDoc[field.name],
req,
schemaPath: parentSchemaPath,
siblingData,
siblingDocWithLocales,
skipValidation,
value: siblingData[field.name],
})
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
break
}
case 'tab': { case 'tab': {
let tabSiblingData = siblingData let tabSiblingData = siblingData
let tabSiblingDoc = siblingDoc let tabSiblingDoc = siblingDoc
@@ -435,51 +480,6 @@ export const promise = async ({
break break
} }
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeChange?.length) {
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
docWithLocales,
errors,
field,
global,
mergeLocaleActions,
operation,
originalDoc: doc,
path: fieldPath,
previousSiblingDoc: siblingDoc,
previousValue: siblingDoc[field.name],
req,
schemaPath: parentSchemaPath,
siblingData,
siblingDocWithLocales,
skipValidation,
value: siblingData[field.name],
})
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
break
}
default: { default: {
break break
} }

View File

@@ -145,26 +145,6 @@ export const promise = async <T>({
localization.localeCodes.forEach((locale) => { localization.localeCodes.forEach((locale) => {
if (fieldData[locale]) { if (fieldData[locale]) {
switch (field.type) { switch (field.type) {
case 'tab':
case 'group': {
promises.push(
traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldSchemaPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc: fieldData[locale],
}),
)
break
}
case 'array': { case 'array': {
const rows = fieldData[locale] const rows = fieldData[locale]
@@ -189,7 +169,6 @@ export const promise = async <T>({
} }
break break
} }
case 'blocks': { case 'blocks': {
const rows = fieldData[locale] const rows = fieldData[locale]
@@ -220,6 +199,27 @@ export const promise = async <T>({
} }
break break
} }
case 'group':
case 'tab': {
promises.push(
traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldSchemaPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc: fieldData[locale],
}),
)
break
}
} }
} }
}) })
@@ -230,30 +230,6 @@ export const promise = async <T>({
// we need to further traverse its children // we need to further traverse its children
// so the child fields can run beforeDuplicate hooks // so the child fields can run beforeDuplicate hooks
switch (field.type) { switch (field.type) {
case 'tab':
case 'group': {
if (typeof siblingDoc[field.name] !== 'object') {
siblingDoc[field.name] = {}
}
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
await traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc: groupDoc as JsonObject,
})
break
}
case 'array': { case 'array': {
const rows = siblingDoc[field.name] const rows = siblingDoc[field.name]
@@ -279,7 +255,6 @@ export const promise = async <T>({
} }
break break
} }
case 'blocks': { case 'blocks': {
const rows = siblingDoc[field.name] const rows = siblingDoc[field.name]
@@ -313,13 +288,38 @@ export const promise = async <T>({
break break
} }
case 'group':
case 'tab': {
if (typeof siblingDoc[field.name] !== 'object') {
siblingDoc[field.name] = {}
}
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
await traverseFields({
id,
collection,
context,
doc,
fields: field.fields,
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingDoc: groupDoc as JsonObject,
})
break
}
} }
} }
} else { } else {
// Finally, we traverse fields which do not affect data here // Finally, we traverse fields which do not affect data here
switch (field.type) { switch (field.type) {
case 'row': case 'collapsible':
case 'collapsible': { case 'row': {
await traverseFields({ await traverseFields({
id, id,
collection, collection,

View File

@@ -90,6 +90,31 @@ export const promise = async <T>({
// Sanitize incoming data // Sanitize incoming data
switch (field.type) { switch (field.type) {
case 'array':
case 'blocks': {
// Handle cases of arrays being intentionally set to 0
if (siblingData[field.name] === '0' || siblingData[field.name] === 0) {
siblingData[field.name] = []
}
break
}
case 'checkbox': {
if (siblingData[field.name] === 'true') {
siblingData[field.name] = true
}
if (siblingData[field.name] === 'false') {
siblingData[field.name] = false
}
if (siblingData[field.name] === '') {
siblingData[field.name] = false
}
break
}
case 'number': { case 'number': {
if (typeof siblingData[field.name] === 'string') { if (typeof siblingData[field.name] === 'string') {
const value = siblingData[field.name] as string const value = siblingData[field.name] as string
@@ -114,36 +139,8 @@ export const promise = async <T>({
break break
} }
case 'checkbox': {
if (siblingData[field.name] === 'true') {
siblingData[field.name] = true
}
if (siblingData[field.name] === 'false') {
siblingData[field.name] = false
}
if (siblingData[field.name] === '') {
siblingData[field.name] = false
}
break
}
case 'richText': {
if (typeof siblingData[field.name] === 'string') {
try {
const richTextJSON = JSON.parse(siblingData[field.name] as string)
siblingData[field.name] = richTextJSON
} catch {
// Disregard this data as it is not valid.
// Will be reported to user by field validation
}
}
break
}
case 'relationship': case 'relationship':
case 'upload': { case 'upload': {
if ( if (
siblingData[field.name] === '' || siblingData[field.name] === '' ||
@@ -230,12 +227,15 @@ export const promise = async <T>({
} }
break break
} }
case 'richText': {
case 'array': if (typeof siblingData[field.name] === 'string') {
case 'blocks': { try {
// Handle cases of arrays being intentionally set to 0 const richTextJSON = JSON.parse(siblingData[field.name] as string)
if (siblingData[field.name] === '0' || siblingData[field.name] === 0) { siblingData[field.name] = richTextJSON
siblingData[field.name] = [] } catch {
// Disregard this data as it is not valid.
// Will be reported to user by field validation
}
} }
break break
@@ -305,37 +305,6 @@ export const promise = async <T>({
// Traverse subfields // Traverse subfields
switch (field.type) { switch (field.type) {
case 'group': {
if (typeof siblingData[field.name] !== 'object') {
siblingData[field.name] = {}
}
if (typeof siblingDoc[field.name] !== 'object') {
siblingDoc[field.name] = {}
}
const groupData = siblingData[field.name] as Record<string, unknown>
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
await traverseFields({
id,
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingData: groupData as JsonObject,
siblingDoc: groupDoc as JsonObject,
})
break
}
case 'array': { case 'array': {
const rows = siblingData[field.name] const rows = siblingData[field.name]
@@ -405,8 +374,9 @@ export const promise = async <T>({
break break
} }
case 'row': case 'collapsible':
case 'collapsible': {
case 'row': {
await traverseFields({ await traverseFields({
id, id,
collection, collection,
@@ -426,6 +396,76 @@ export const promise = async <T>({
break break
} }
case 'group': {
if (typeof siblingData[field.name] !== 'object') {
siblingData[field.name] = {}
}
if (typeof siblingDoc[field.name] !== 'object') {
siblingDoc[field.name] = {}
}
const groupData = siblingData[field.name] as Record<string, unknown>
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
await traverseFields({
id,
collection,
context,
data,
doc,
fields: field.fields,
global,
operation,
overrideAccess,
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingData: groupData as JsonObject,
siblingDoc: groupDoc as JsonObject,
})
break
}
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeValidate?.length) {
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
overrideAccess,
path: fieldPath,
previousSiblingDoc: siblingDoc,
previousValue: siblingData[field.name],
req,
schemaPath: fieldSchemaPath,
siblingData,
value: siblingData[field.name],
})
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
break
}
case 'tab': { case 'tab': {
let tabSiblingData let tabSiblingData
@@ -486,46 +526,6 @@ export const promise = async <T>({
break break
} }
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeValidate?.length) {
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
overrideAccess,
path: fieldPath,
previousSiblingDoc: siblingDoc,
previousValue: siblingData[field.name],
req,
schemaPath: fieldSchemaPath,
siblingData,
value: siblingData[field.name],
})
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
break
}
default: { default: {
break break
} }

View File

@@ -239,123 +239,125 @@ export function fieldsToJSONSchema(
let fieldSchema: JSONSchema4 let fieldSchema: JSONSchema4
switch (field.type) { switch (field.type) {
case 'text': case 'array': {
if (field.hasMany === true) {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: { type: 'string' },
}
} else {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
}
break
case 'textarea':
case 'code':
case 'email':
case 'date': {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
break
}
case 'number': {
if (field.hasMany === true) {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: { type: 'number' },
}
} else {
fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }
}
break
}
case 'checkbox': {
fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }
break
}
case 'json': {
fieldSchema = field.jsonSchema?.schema || {
type: ['object', 'array', 'string', 'number', 'boolean', 'null'],
}
break
}
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
if (field.editor.outputSchema) {
fieldSchema = field.editor.outputSchema({
collectionIDFieldTypes,
config,
field,
interfaceNameDefinitions,
isRequired,
})
} else {
// Maintain backwards compatibility with existing rich text editors
fieldSchema = { fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired), type: withNullableJSONSchemaType('array', isRequired),
items: { items: {
type: 'object', type: 'object',
additionalProperties: false,
...fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
),
}, },
} }
}
break if (field.interfaceName) {
} interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
case 'radio': {
fieldSchema = { fieldSchema = {
type: withNullableJSONSchemaType('string', isRequired), $ref: `#/definitions/${field.interfaceName}`,
enum: buildOptionEnums(field.options), }
} }
break break
} }
case 'blocks': {
// Check for a case where no blocks are provided.
// We need to generate an empty array for this case, note that JSON schema 4 doesn't support empty arrays
// so the best we can get is `unknown[]`
const hasBlocks = Boolean(field.blocks.length)
case 'select': {
const optionEnums = buildOptionEnums(field.options)
if (field.hasMany) {
fieldSchema = { fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired), type: withNullableJSONSchemaType('array', isRequired),
items: { items: hasBlocks
type: 'string', ? {
oneOf: field.blocks.map((block) => {
const blockFieldSchemas = fieldsToJSONSchema(
collectionIDFieldTypes,
block.fields,
interfaceNameDefinitions,
config,
)
const blockSchema: JSONSchema4 = {
type: 'object',
additionalProperties: false,
properties: {
...blockFieldSchemas.properties,
blockType: {
const: block.slug,
}, },
},
required: ['blockType', ...blockFieldSchemas.required],
} }
if (optionEnums?.length) {
;(fieldSchema.items as JSONSchema4).enum = optionEnums if (block.interfaceName) {
} interfaceNameDefinitions.set(block.interfaceName, blockSchema)
} else {
fieldSchema = { return {
type: withNullableJSONSchemaType('string', isRequired), $ref: `#/definitions/${block.interfaceName}`,
}
if (optionEnums?.length) {
fieldSchema.enum = optionEnums
} }
} }
return blockSchema
}),
}
: {},
}
break
}
case 'checkbox': {
fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }
break
}
case 'code':
case 'date':
case 'email':
case 'textarea': {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
break break
} }
case 'point': { case 'collapsible':
case 'row': {
const childSchema = fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
)
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
fieldSchemas.set(propName, propSchema)
})
childSchema.required.forEach((propName) => {
requiredFieldNames.add(propName)
})
break
}
case 'group': {
fieldSchema = { fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired), type: 'object',
items: [ additionalProperties: false,
{ ...fieldsToJSONSchema(
type: 'number', collectionIDFieldTypes,
}, field.fields,
{ interfaceNameDefinitions,
type: 'number', config,
}, ),
], }
maxItems: 2,
minItems: 2, if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
}
} }
break break
} }
@@ -384,8 +386,53 @@ export function fieldsToJSONSchema(
break break
} }
case 'upload': case 'json': {
case 'relationship': { fieldSchema = field.jsonSchema?.schema || {
type: ['object', 'array', 'string', 'number', 'boolean', 'null'],
}
break
}
case 'number': {
if (field.hasMany === true) {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: { type: 'number' },
}
} else {
fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }
}
break
}
case 'point': {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: [
{
type: 'number',
},
{
type: 'number',
},
],
maxItems: 2,
minItems: 2,
}
break
}
case 'radio': {
fieldSchema = {
type: withNullableJSONSchemaType('string', isRequired),
enum: buildOptionEnums(field.options),
}
break
}
case 'relationship':
case 'upload': {
if (Array.isArray(field.relationTo)) { if (Array.isArray(field.relationTo)) {
if (field.hasMany) { if (field.hasMany) {
fieldSchema = { fieldSchema = {
@@ -474,91 +521,55 @@ export function fieldsToJSONSchema(
break break
} }
case 'blocks': { case 'richText': {
// Check for a case where no blocks are provided. if (!field?.editor) {
// We need to generate an empty array for this case, note that JSON schema 4 doesn't support empty arrays throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
// so the best we can get is `unknown[]` }
const hasBlocks = Boolean(field.blocks.length) if (typeof field.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
fieldSchema = { }
type: withNullableJSONSchemaType('array', isRequired), if (field.editor.outputSchema) {
items: hasBlocks fieldSchema = field.editor.outputSchema({
? {
oneOf: field.blocks.map((block) => {
const blockFieldSchemas = fieldsToJSONSchema(
collectionIDFieldTypes, collectionIDFieldTypes,
block.fields,
interfaceNameDefinitions,
config, config,
) field,
interfaceNameDefinitions,
const blockSchema: JSONSchema4 = { isRequired,
type: 'object', })
additionalProperties: false, } else {
properties: { // Maintain backwards compatibility with existing rich text editors
...blockFieldSchemas.properties,
blockType: {
const: block.slug,
},
},
required: ['blockType', ...blockFieldSchemas.required],
}
if (block.interfaceName) {
interfaceNameDefinitions.set(block.interfaceName, blockSchema)
return {
$ref: `#/definitions/${block.interfaceName}`,
}
}
return blockSchema
}),
}
: {},
}
break
}
case 'array': {
fieldSchema = { fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired), type: withNullableJSONSchemaType('array', isRequired),
items: { items: {
type: 'object', type: 'object',
additionalProperties: false,
...fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
),
}, },
} }
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
}
} }
break break
} }
case 'select': {
const optionEnums = buildOptionEnums(field.options)
if (field.hasMany) {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: {
type: 'string',
},
}
if (optionEnums?.length) {
;(fieldSchema.items as JSONSchema4).enum = optionEnums
}
} else {
fieldSchema = {
type: withNullableJSONSchemaType('string', isRequired),
}
if (optionEnums?.length) {
fieldSchema.enum = optionEnums
}
}
case 'row':
case 'collapsible': {
const childSchema = fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
)
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
fieldSchemas.set(propName, propSchema)
})
childSchema.required.forEach((propName) => {
requiredFieldNames.add(propName)
})
break break
} }
@@ -596,27 +607,16 @@ export function fieldsToJSONSchema(
break break
} }
case 'group': { case 'text':
if (field.hasMany === true) {
fieldSchema = { fieldSchema = {
type: 'object', type: withNullableJSONSchemaType('array', isRequired),
additionalProperties: false, items: { type: 'string' },
...fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
),
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
} }
} else {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
} }
break break
}
default: { default: {
break break
@@ -704,15 +704,6 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS
for (const field of fields) { for (const field of fields) {
switch (field.type) { switch (field.type) {
case 'row':
case 'collapsible':
schema.properties = {
...schema.properties,
...fieldsToSelectJSONSchema({ fields: field.fields }).properties,
}
break
case 'array': case 'array':
case 'group': case 'group':
schema.properties[field.name] = { schema.properties[field.name] = {
@@ -725,27 +716,6 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS
} }
break break
case 'tabs':
for (const tab of field.tabs) {
if (tabHasName(tab)) {
schema.properties[tab.name] = {
oneOf: [
{
type: 'boolean',
},
fieldsToSelectJSONSchema({ fields: tab.fields }),
],
}
continue
}
schema.properties = {
...schema.properties,
...fieldsToSelectJSONSchema({ fields: tab.fields }).properties,
}
}
break
case 'blocks': { case 'blocks': {
const blocksSchema: JSONSchema4 = { const blocksSchema: JSONSchema4 = {
type: 'object', type: 'object',
@@ -775,6 +745,36 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS
break break
} }
case 'collapsible':
case 'row':
schema.properties = {
...schema.properties,
...fieldsToSelectJSONSchema({ fields: field.fields }).properties,
}
break
case 'tabs':
for (const tab of field.tabs) {
if (tabHasName(tab)) {
schema.properties[tab.name] = {
oneOf: [
{
type: 'boolean',
},
fieldsToSelectJSONSchema({ fields: tab.fields }),
],
}
continue
}
schema.properties = {
...schema.properties,
...fieldsToSelectJSONSchema({ fields: tab.fields }).properties,
}
}
break
default: default:
schema.properties[field.name] = { schema.properties[field.name] = {
@@ -800,6 +800,34 @@ const generateAuthFieldTypes = ({
}): JSONSchema4 => { }): JSONSchema4 => {
if (loginWithUsername) { if (loginWithUsername) {
switch (type) { switch (type) {
case 'forgotOrUnlock': {
if (loginWithUsername.allowEmailLogin) {
// allow email or username for unlock/forgot-password
return {
additionalProperties: false,
oneOf: [
{
additionalProperties: false,
properties: { email: fieldType },
required: ['email'],
},
{
additionalProperties: false,
properties: { username: fieldType },
required: ['username'],
},
],
}
} else {
// allow only username for unlock/forgot-password
return {
additionalProperties: false,
properties: { username: fieldType },
required: ['username'],
}
}
}
case 'login': { case 'login': {
if (loginWithUsername.allowEmailLogin) { if (loginWithUsername.allowEmailLogin) {
// allow username or email and require password for login // allow username or email and require password for login
@@ -858,34 +886,6 @@ const generateAuthFieldTypes = ({
required: requiredFields, required: requiredFields,
} }
} }
case 'forgotOrUnlock': {
if (loginWithUsername.allowEmailLogin) {
// allow email or username for unlock/forgot-password
return {
additionalProperties: false,
oneOf: [
{
additionalProperties: false,
properties: { email: fieldType },
required: ['email'],
},
{
additionalProperties: false,
properties: { username: fieldType },
required: ['username'],
},
],
}
} else {
// allow only username for unlock/forgot-password
return {
additionalProperties: false,
properties: { username: fieldType },
required: ['username'],
}
}
}
} }
} }

View File

@@ -16,15 +16,6 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
let result = acc let result = acc
switch (field.type) { switch (field.type) {
case 'group':
acc.push({
name: field.name,
type: field.type,
fields: fieldSchemaToJSON(field.fields),
})
break
case 'array': case 'array':
acc.push({ acc.push({
name: field.name, name: field.name,
@@ -61,11 +52,31 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
break break
case 'row':
case 'collapsible': case 'collapsible':
case 'row':
result = result.concat(fieldSchemaToJSON(field.fields)) result = result.concat(fieldSchemaToJSON(field.fields))
break break
case 'group':
acc.push({
name: field.name,
type: field.type,
fields: fieldSchemaToJSON(field.fields),
})
break
case 'relationship':
case 'upload':
acc.push({
name: field.name,
type: field.type,
hasMany: 'hasMany' in field ? Boolean(field.hasMany) : false, // TODO: type this
relationTo: field.relationTo,
})
break
case 'tabs': { case 'tabs': {
let tabFields = [] let tabFields = []
@@ -87,17 +98,6 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
break break
} }
case 'relationship':
case 'upload':
acc.push({
name: field.name,
type: field.type,
hasMany: 'hasMany' in field ? Boolean(field.hasMany) : false, // TODO: type this
relationTo: field.relationTo,
})
break
default: default:
if ('name' in field) { if ('name' in field) {
acc.push({ acc.push({

View File

@@ -21,7 +21,7 @@ import { envPaths } from './envPaths.js'
const createPlainObject = <T = Record<string, unknown>>(): T => Object.create(null) const createPlainObject = <T = Record<string, unknown>>(): T => Object.create(null)
const checkValueType = (key: string, value: unknown): void => { const checkValueType = (key: string, value: unknown): void => {
const nonJsonTypes = new Set(['undefined', 'symbol', 'function']) const nonJsonTypes = new Set(['function', 'symbol', 'undefined'])
const type = typeof value const type = typeof value

View File

@@ -28,16 +28,16 @@ export const getPaymentTotal = (
total += valueToUse total += valueToUse
break break
} }
case 'subtract': { case 'divide': {
total -= valueToUse total /= valueToUse
break break
} }
case 'multiply': { case 'multiply': {
total *= valueToUse total *= valueToUse
break break
} }
case 'divide': { case 'subtract': {
total /= valueToUse total -= valueToUse
break break
} }
default: { default: {

View File

@@ -91,6 +91,30 @@ export const serializeSlate = (children?: Node[], submissionData?: any): string
<h6> <h6>
${serializeSlate(node.children, submissionData)} ${serializeSlate(node.children, submissionData)}
</h6> </h6>
`
case 'indent':
return `
<p style="padding-left: 20px">
${serializeSlate(node.children, submissionData)}
</p>
`
case 'li':
return `
<li>
${serializeSlate(node.children, submissionData)}
</li>
`
case 'link':
return `
<a href={${escapeHTML(replaceDoubleCurlys(node.url, submissionData))}}>
${serializeSlate(node.children, submissionData)}
</a>
`
case 'ol':
return `
<ol>
${serializeSlate(node.children, submissionData)}
</ol>
` `
case 'quote': case 'quote':
return ` return `
@@ -104,30 +128,6 @@ export const serializeSlate = (children?: Node[], submissionData?: any): string
${serializeSlate(node.children, submissionData)} ${serializeSlate(node.children, submissionData)}
</ul> </ul>
` `
case 'ol':
return `
<ol>
${serializeSlate(node.children, submissionData)}
</ol>
`
case 'li':
return `
<li>
${serializeSlate(node.children, submissionData)}
</li>
`
case 'indent':
return `
<p style="padding-left: 20px">
${serializeSlate(node.children, submissionData)}
</p>
`
case 'link':
return `
<a href={${escapeHTML(replaceDoubleCurlys(node.url, submissionData))}}>
${serializeSlate(node.children, submissionData)}
</a>
`
default: default:
return ` return `

View File

@@ -30,8 +30,8 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
}) })
break break
} }
case 'updated': { case 'deleted': {
void handleCreatedOrUpdated({ void handleDeleted({
...args, ...args,
pluginConfig, pluginConfig,
resourceType, resourceType,
@@ -39,8 +39,8 @@ export const handleWebhooks: StripeWebhookHandler = (args) => {
}) })
break break
} }
case 'deleted': { case 'updated': {
void handleDeleted({ void handleCreatedOrUpdated({
...args, ...args,
pluginConfig, pluginConfig,
resourceType, resourceType,

View File

@@ -93,7 +93,7 @@
"babel-plugin-transform-remove-imports": "^1.8.0", "babel-plugin-transform-remove-imports": "^1.8.0",
"esbuild": "0.23.1", "esbuild": "0.23.1",
"esbuild-sass-plugin": "3.3.1", "esbuild-sass-plugin": "3.3.1",
"eslint-plugin-react-compiler": "0.0.0-experimental-7670337-20240918", "eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"payload": "workspace:*", "payload": "workspace:*",
"swc-plugin-transform-remove-imports": "1.15.0" "swc-plugin-transform-remove-imports": "1.15.0"
}, },

View File

@@ -45,17 +45,14 @@ const formatStep = (step: Step) => {
case 'click': { case 'click': {
return ` await page.mouse.click(${value.x}, ${value.y});` return ` await page.mouse.click(${value.x}, ${value.y});`
} }
case 'press': {
return ` await page.keyboard.press('${value}');`
}
case 'keydown': { case 'keydown': {
return ` await page.keyboard.keydown('${value}');` return ` await page.keyboard.keydown('${value}');`
} }
case 'keyup': { case 'keyup': {
return ` await page.keyboard.keyup('${value}');` return ` await page.keyboard.keyup('${value}');`
} }
case 'type': { case 'press': {
return ` await page.keyboard.type('${value}');` return ` await page.keyboard.press('${value}');`
} }
case 'selectAll': { case 'selectAll': {
return ` await selectAll(page);` return ` await selectAll(page);`
@@ -70,6 +67,9 @@ const formatStep = (step: Step) => {
}); });
` `
} }
case 'type': {
return ` await page.keyboard.type('${value}');`
}
default: default:
return `` return ``
} }
@@ -119,14 +119,14 @@ function getPathFromNodeToEditor(node: Node, rootElement: HTMLElement | null) {
} }
const keyPresses = new Set([ const keyPresses = new Set([
'Enter', 'ArrowDown',
'Backspace',
'Delete',
'Escape',
'ArrowLeft', 'ArrowLeft',
'ArrowRight', 'ArrowRight',
'ArrowUp', 'ArrowUp',
'ArrowDown', 'Backspace',
'Delete',
'Enter',
'Escape',
]) ])
type Step = { type Step = {

View File

@@ -84,7 +84,7 @@ export const ToolbarButton = ({
className={className} className={className}
onClick={() => { onClick={() => {
if (enabled !== false) { if (enabled !== false) {
editor._updateTags = new Set([...editor._updateTags, 'toolbar']) // without setting the tags, our onSelect will not be able to trigger our onChange as focus onChanges are ignored. editor._updateTags = new Set(['toolbar', ...editor._updateTags]) // without setting the tags, our onSelect will not be able to trigger our onChange as focus onChanges are ignored.
editor.focus(() => { editor.focus(() => {
// We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called. // We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called.

View File

@@ -65,7 +65,7 @@ export function DropDownItem({
className={className} className={className}
onClick={() => { onClick={() => {
if (enabled !== false) { if (enabled !== false) {
editor._updateTags = new Set([...editor._updateTags, 'toolbar']) // without setting the tags, our onSelect will not be able to trigger our onChange as focus onChanges are ignored. editor._updateTags = new Set(['toolbar', ...editor._updateTags]) // without setting the tags, our onSelect will not be able to trigger our onChange as focus onChanges are ignored.
editor.focus(() => { editor.focus(() => {
// We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called. // We need to wrap the onSelect in the callback, so the editor is properly focused before the onSelect is called.

View File

@@ -124,27 +124,27 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
if (element.textAlign) { if (element.textAlign) {
if (element.type === 'relationship' || element.type === 'upload') { if (element.type === 'relationship' || element.type === 'upload') {
switch (element.textAlign) { switch (element.textAlign) {
case 'center':
attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
break
case 'left': case 'left':
attr = { ...attr, style: { marginRight: 'auto' } } attr = { ...attr, style: { marginRight: 'auto' } }
break break
case 'right': case 'right':
attr = { ...attr, style: { marginLeft: 'auto' } } attr = { ...attr, style: { marginLeft: 'auto' } }
break break
case 'center':
attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
break
default: default:
attr = { ...attr, style: { textAlign: element.textAlign } } attr = { ...attr, style: { textAlign: element.textAlign } }
break break
} }
} else if (element.type === 'li') { } else if (element.type === 'li') {
switch (element.textAlign) { switch (element.textAlign) {
case 'right':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
break
case 'center': case 'center':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'center' } } attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'center' } }
break break
case 'right':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
break
case 'left': case 'left':
default: default:
attr = { ...attr, style: { listStylePosition: 'outside', textAlign: 'left' } } attr = { ...attr, style: { listStylePosition: 'outside', textAlign: 'left' } }

View File

@@ -150,6 +150,9 @@ export const RscEntrySlateField: React.FC<
break break
} }
case 'relationship':
break
case 'upload': { case 'upload': {
const uploadEnabledCollections = payload.config.collections.filter( const uploadEnabledCollections = payload.config.collections.filter(
({ admin: { enableRichTextRelationship, hidden }, upload }) => { ({ admin: { enableRichTextRelationship, hidden }, upload }) => {
@@ -179,9 +182,6 @@ export const RscEntrySlateField: React.FC<
break break
} }
case 'relationship':
break
} }
} }
}) })

View File

@@ -42,6 +42,9 @@ export const getGenerateSchemaMap =
break break
} }
case 'relationship':
break
case 'upload': { case 'upload': {
const uploadEnabledCollections = config.collections.filter( const uploadEnabledCollections = config.collections.filter(
({ admin: { enableRichTextRelationship, hidden }, upload }) => { ({ admin: { enableRichTextRelationship, hidden }, upload }) => {
@@ -73,9 +76,6 @@ export const getGenerateSchemaMap =
break break
} }
case 'relationship':
break
} }
} }
}) })

View File

@@ -161,12 +161,12 @@ export type InitTFunction<
} }
export type InitI18n = export type InitI18n =
| ((args: { config: I18nOptions; context: 'api'; language: AcceptedLanguages }) => Promise<I18n>)
| ((args: { | ((args: {
config: I18nOptions<ClientTranslationsObject> config: I18nOptions<ClientTranslationsObject>
context: 'client' context: 'client'
language: AcceptedLanguages language: AcceptedLanguages
}) => Promise<I18n<ClientTranslationsObject, ClientTranslationKeys>>) }) => Promise<I18n<ClientTranslationsObject, ClientTranslationKeys>>)
| ((args: { config: I18nOptions; context: 'api'; language: AcceptedLanguages }) => Promise<I18n>)
export type LanguagePreference = { export type LanguagePreference = {
language: AcceptedLanguages language: AcceptedLanguages

View File

@@ -122,7 +122,7 @@
"babel-plugin-react-compiler": "0.0.0-experimental-24ec0eb-20240918", "babel-plugin-react-compiler": "0.0.0-experimental-24ec0eb-20240918",
"esbuild": "0.23.1", "esbuild": "0.23.1",
"esbuild-sass-plugin": "3.3.1", "esbuild-sass-plugin": "3.3.1",
"eslint-plugin-react-compiler": "0.0.0-experimental-7670337-20240918", "eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"payload": "workspace:*" "payload": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import LinkImport from 'next/link.js' // TODO: abstract this out to support all routers
import type { MouseEvent } from 'react' import type { MouseEvent } from 'react'
import LinkImport from 'next/link.js' // TODO: abstract this out to support all routers
import React from 'react' import React from 'react'
import './index.scss' import './index.scss'

View File

@@ -26,11 +26,11 @@ export function AddingFilesView() {
activeIndex, activeIndex,
collectionSlug, collectionSlug,
docPermissions, docPermissions,
documentSlots,
forms, forms,
hasPublishPermission, hasPublishPermission,
hasSavePermission, hasSavePermission,
hasSubmitted, hasSubmitted,
documentSlots,
} = useFormsManager() } = useFormsManager()
const activeForm = forms[activeIndex] const activeForm = forms[activeIndex]
const { getEntityConfig } = useConfig() const { getEntityConfig } = useConfig()

View File

@@ -35,18 +35,6 @@ type Action =
export function formsManagementReducer(state: State, action: Action): State { export function formsManagementReducer(state: State, action: Action): State {
switch (action.type) { switch (action.type) {
case 'REPLACE': {
return {
...state,
...action.state,
}
}
case 'SET_ACTIVE_INDEX': {
return {
...state,
activeIndex: action.index,
}
}
case 'ADD_FORMS': { case 'ADD_FORMS': {
const newForms: State['forms'] = [] const newForms: State['forms'] = []
for (let i = 0; i < action.files.length; i++) { for (let i = 0; i < action.files.length; i++) {
@@ -89,6 +77,18 @@ export function formsManagementReducer(state: State, action: Action): State {
totalErrorCount: state.totalErrorCount - removedForm.errorCount, totalErrorCount: state.totalErrorCount - removedForm.errorCount,
} }
} }
case 'REPLACE': {
return {
...state,
...action.state,
}
}
case 'SET_ACTIVE_INDEX': {
return {
...state,
activeIndex: action.index,
}
}
case 'UPDATE_ERROR_COUNT': { case 'UPDATE_ERROR_COUNT': {
const forms = [...state.forms] const forms = [...state.forms]
forms[action.index].errorCount = action.count forms[action.index].errorCount = action.count

View File

@@ -8,6 +8,8 @@ import { useRouter } from 'next/navigation.js'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js'
import { useForm, useFormModified } from '../../forms/Form/context.js' import { useForm, useFormModified } from '../../forms/Form/context.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js' import { useEditDepth } from '../../providers/EditDepth/index.js'
@@ -18,7 +20,6 @@ import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { Button } from '../Button/index.js' import { Button } from '../Button/index.js'
import { drawerZBase } from '../Drawer/index.js' import { drawerZBase } from '../Drawer/index.js'
import { PopupList } from '../Popup/index.js' import { PopupList } from '../Popup/index.js'
import { DocumentDrawerContextType } from '../DocumentDrawer/Provider.jsx'
import './index.scss' import './index.scss'
const baseClass = 'duplicate' const baseClass = 'duplicate'

View File

@@ -119,8 +119,8 @@ export const RelationshipCell: React.FC<RelationshipCellProps> = ({
if (previewAllowed && document) { if (previewAllowed && document) {
fileField = ( fileField = (
<FileCell <FileCell
collectionConfig={relatedCollection}
cellData={label} cellData={label}
collectionConfig={relatedCollection}
customCellProps={customCellContext} customCellProps={customCellContext}
field={field} field={field}
rowData={document} rowData={document}

View File

@@ -1,18 +1,16 @@
'use client' 'use client'
import LinkImport from 'next/link.js'
import React from 'react' // TODO: abstract this out to support all routers
import type { DefaultCellComponentProps, UploadFieldClient } from 'payload' import type { DefaultCellComponentProps, UploadFieldClient } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import LinkImport from 'next/link.js'
import { fieldAffectsData } from 'payload/shared' import { fieldAffectsData } from 'payload/shared'
import React from 'react' // TODO: abstract this out to support all routers
import { useConfig } from '../../../providers/Config/index.js' import { useConfig } from '../../../providers/Config/index.js'
import { useTranslation } from '../../../providers/Translation/index.js' import { useTranslation } from '../../../providers/Translation/index.js'
import { formatAdminURL } from '../../../utilities/formatAdminURL.js' import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
import { CodeCell } from './fields/Code/index.js' import { CodeCell } from './fields/Code/index.js'
import { cellComponents } from './fields/index.js' import { cellComponents } from './fields/index.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const DefaultCell: React.FC<DefaultCellComponentProps> = (props) => { export const DefaultCell: React.FC<DefaultCellComponentProps> = (props) => {

View File

@@ -18,19 +18,6 @@ type Action = AddLoadedDocuments | RequestDocuments
export function reducer(state: Documents, action: Action): Documents { export function reducer(state: Documents, action: Action): Documents {
switch (action.type) { switch (action.type) {
case 'REQUEST': {
const newState = { ...state }
action.docs.forEach(({ relationTo, value }) => {
if (typeof newState[relationTo] !== 'object') {
newState[relationTo] = {}
}
newState[relationTo][value] = null
})
return newState
}
case 'ADD_LOADED': { case 'ADD_LOADED': {
const newState = { ...state } const newState = { ...state }
if (typeof newState[action.relationTo] !== 'object') { if (typeof newState[action.relationTo] !== 'object') {
@@ -52,6 +39,19 @@ export function reducer(state: Documents, action: Action): Documents {
return newState return newState
} }
case 'REQUEST': {
const newState = { ...state }
action.docs.forEach(({ relationTo, value }) => {
if (typeof newState[relationTo] !== 'object') {
newState[relationTo] = {}
}
newState[relationTo][value] = null
})
return newState
}
default: { default: {
return state return state
} }

View File

@@ -43,9 +43,9 @@ export const DefaultFilter: React.FC<Props> = ({
} }
switch (internalField?.field?.type) { switch (internalField?.field?.type) {
case 'number': { case 'date': {
return ( return (
<NumberField <DateField
disabled={disabled} disabled={disabled}
field={internalField.field} field={internalField.field}
onChange={onChange} onChange={onChange}
@@ -55,9 +55,9 @@ export const DefaultFilter: React.FC<Props> = ({
) )
} }
case 'date': { case 'number': {
return ( return (
<DateField <NumberField
disabled={disabled} disabled={disabled}
field={internalField.field} field={internalField.field}
onChange={onChange} onChange={onChange}

View File

@@ -14,10 +14,6 @@ const reduceToIDs = (options) =>
const optionsReducer = (state: Option[], action: Action): Option[] => { const optionsReducer = (state: Option[], action: Action): Option[] => {
switch (action.type) { switch (action.type) {
case 'CLEAR': {
return action.required ? [] : [{ label: action.i18n.t('general:none'), value: 'null' }]
}
case 'ADD': { case 'ADD': {
const { collection, data, hasMultipleRelations, i18n, relation } = action const { collection, data, hasMultipleRelations, i18n, relation } = action
@@ -79,6 +75,10 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
return newOptions return newOptions
} }
case 'CLEAR': {
return action.required ? [] : [{ label: action.i18n.t('general:none'), value: 'null' }]
}
default: { default: {
return state return state
} }

View File

@@ -28,61 +28,6 @@ const sortOptions = (options: Option[]): Option[] =>
export const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] => { export const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] => {
switch (action.type) { switch (action.type) {
case 'CLEAR': {
const exemptValues = action.exemptValues
? Array.isArray(action.exemptValues)
? action.exemptValues
: [action.exemptValues]
: []
const clearedStateWithExemptValues = state.filter((optionGroup) => {
const clearedOptions = optionGroup.options.filter((option) => {
if (exemptValues) {
return exemptValues.some((exemptValue) => {
return (
exemptValue &&
option.value === (typeof exemptValue === 'object' ? exemptValue.value : exemptValue)
)
})
}
return false
})
optionGroup.options = clearedOptions
return clearedOptions.length > 0
})
return clearedStateWithExemptValues
}
case 'UPDATE': {
const { collection, config, doc, i18n } = action
const relation = collection.slug
const newOptions = [...state]
const docTitle = formatDocTitle({
collectionConfig: collection,
data: doc,
dateFormat: config.admin.dateFormat,
fallback: `${i18n.t('general:untitled')} - ID: ${doc.id}`,
i18n,
})
const foundOptionGroup = newOptions.find(
(optionGroup) => optionGroup.label === collection.labels.plural,
)
const foundOption = foundOptionGroup?.options?.find((option) => option.value === doc.id)
if (foundOption) {
foundOption.label = docTitle
foundOption.relationTo = relation
}
return newOptions
}
case 'ADD': { case 'ADD': {
const { collection, config, docs, i18n, ids = [], sort } = action const { collection, config, docs, i18n, ids = [], sort } = action
const relation = collection.slug const relation = collection.slug
@@ -146,6 +91,35 @@ export const optionsReducer = (state: OptionGroup[], action: Action): OptionGrou
return newOptions return newOptions
} }
case 'CLEAR': {
const exemptValues = action.exemptValues
? Array.isArray(action.exemptValues)
? action.exemptValues
: [action.exemptValues]
: []
const clearedStateWithExemptValues = state.filter((optionGroup) => {
const clearedOptions = optionGroup.options.filter((option) => {
if (exemptValues) {
return exemptValues.some((exemptValue) => {
return (
exemptValue &&
option.value === (typeof exemptValue === 'object' ? exemptValue.value : exemptValue)
)
})
}
return false
})
optionGroup.options = clearedOptions
return clearedOptions.length > 0
})
return clearedStateWithExemptValues
}
case 'REMOVE': { case 'REMOVE': {
const { id, collection } = action const { id, collection } = action
@@ -167,6 +141,32 @@ export const optionsReducer = (state: OptionGroup[], action: Action): OptionGrou
return newOptions return newOptions
} }
case 'UPDATE': {
const { collection, config, doc, i18n } = action
const relation = collection.slug
const newOptions = [...state]
const docTitle = formatDocTitle({
collectionConfig: collection,
data: doc,
dateFormat: config.admin.dateFormat,
fallback: `${i18n.t('general:untitled')} - ID: ${doc.id}`,
i18n,
})
const foundOptionGroup = newOptions.find(
(optionGroup) => optionGroup.label === collection.labels.plural,
)
const foundOption = foundOptionGroup?.options?.find((option) => option.value === doc.id)
if (foundOption) {
foundOption.label = docTitle
foundOption.relationTo = relation
}
return newOptions
}
default: { default: {
return state return state
} }

View File

@@ -17,36 +17,53 @@ const ObjectId = (ObjectIdImport.default ||
*/ */
export function fieldReducer(state: FormState, action: FieldAction): FormState { export function fieldReducer(state: FormState, action: FieldAction): FormState {
switch (action.type) { switch (action.type) {
case 'REPLACE_STATE': { case 'ADD_ROW': {
if (action.optimize !== false) { const { blockType, path, rowIndex: rowIndexFromArgs, subFieldState = {} } = action
// Only update fields that have changed
// by comparing old value / initialValue to new
// ..
// This is a performance enhancement for saving
// large documents with hundreds of fields
const newState = {}
Object.entries(action.state).forEach(([path, field]) => { const rowIndex =
const oldField = state[path] typeof rowIndexFromArgs === 'number' ? rowIndexFromArgs : state[path]?.rows?.length || 0
const newField = field
if (!dequal(oldField, newField)) { const withNewRow = [...(state[path]?.rows || [])]
newState[path] = newField
} else if (oldField) { const newRow: Row = {
newState[path] = oldField id: (subFieldState?.id?.value as string) || new ObjectId().toHexString(),
} blockType: blockType || undefined,
}) collapsed: false,
return newState
}
// If we're not optimizing, just set the state to the new state
return action.state
} }
case 'REMOVE': { withNewRow.splice(rowIndex, 0, newRow)
const newState = { ...state }
if (newState[action.path]) { if (blockType) {
delete newState[action.path] subFieldState.blockType = {
initialValue: blockType,
valid: true,
value: blockType,
} }
}
// add new row to array _field state_
const { remainingFields, rows: siblingRows } = separateRows(path, state)
siblingRows.splice(rowIndex, 0, subFieldState)
const newState: FormState = {
...remainingFields,
...flattenRows(path, siblingRows),
[`${path}.${rowIndex}.id`]: {
initialValue: newRow.id,
passesCondition: true,
requiresRender: true,
valid: true,
value: newRow.id,
},
[path]: {
...state[path],
disableFormData: true,
requiresRender: true,
rows: withNewRow,
value: siblingRows.length,
},
}
return newState return newState
} }
@@ -112,160 +129,6 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
return newState return newState
} }
case 'UPDATE': {
const newField = Object.entries(action).reduce(
(field, [key, value]) => {
if (
[
'disableFormData',
'errorMessage',
'initialValue',
'rows',
'valid',
'validate',
'value',
].includes(key)
) {
return {
...field,
[key]: value,
}
}
return field
},
state[action.path] || ({} as FormField),
)
const newState = {
...state,
[action.path]: newField,
}
return newState
}
case 'UPDATE_MANY': {
const newState = { ...state }
Object.entries(action.formState).forEach(([path, field]) => {
newState[path] = field
})
return newState
}
case 'REMOVE_ROW': {
const { path, rowIndex } = action
const { remainingFields, rows } = separateRows(path, state)
const rowsMetadata = [...(state[path]?.rows || [])]
rows.splice(rowIndex, 1)
rowsMetadata.splice(rowIndex, 1)
const newState: FormState = {
...remainingFields,
[path]: {
...state[path],
disableFormData: rows.length > 0,
requiresRender: true,
rows: rowsMetadata,
value: rows.length,
},
...flattenRows(path, rows),
}
return newState
}
case 'ADD_ROW': {
const { blockType, path, rowIndex: rowIndexFromArgs, subFieldState = {} } = action
const rowIndex =
typeof rowIndexFromArgs === 'number' ? rowIndexFromArgs : state[path]?.rows?.length || 0
const withNewRow = [...(state[path]?.rows || [])]
const newRow: Row = {
id: (subFieldState?.id?.value as string) || new ObjectId().toHexString(),
blockType: blockType || undefined,
collapsed: false,
}
withNewRow.splice(rowIndex, 0, newRow)
if (blockType) {
subFieldState.blockType = {
initialValue: blockType,
valid: true,
value: blockType,
}
}
// add new row to array _field state_
const { remainingFields, rows: siblingRows } = separateRows(path, state)
siblingRows.splice(rowIndex, 0, subFieldState)
const newState: FormState = {
...remainingFields,
...flattenRows(path, siblingRows),
[`${path}.${rowIndex}.id`]: {
initialValue: newRow.id,
passesCondition: true,
requiresRender: true,
valid: true,
value: newRow.id,
},
[path]: {
...state[path],
disableFormData: true,
requiresRender: true,
rows: withNewRow,
value: siblingRows.length,
},
}
return newState
}
case 'REPLACE_ROW': {
const { blockType, path, rowIndex: rowIndexArg, subFieldState = {} } = action
const { remainingFields, rows: siblingRows } = separateRows(path, state)
const rowIndex = Math.max(0, Math.min(rowIndexArg, siblingRows?.length - 1 || 0))
const rowsMetadata = [...(state[path]?.rows || [])]
rowsMetadata[rowIndex] = {
id: new ObjectId().toHexString(),
blockType: blockType || undefined,
collapsed: false,
}
if (blockType) {
subFieldState.blockType = {
initialValue: blockType,
valid: true,
value: blockType,
}
}
// replace form _field state_
siblingRows[rowIndex] = subFieldState
const newState: FormState = {
...remainingFields,
...flattenRows(path, siblingRows),
[path]: {
...state[path],
disableFormData: true,
rows: rowsMetadata,
value: siblingRows.length,
},
}
return newState
}
case 'DUPLICATE_ROW': { case 'DUPLICATE_ROW': {
const { path, rowIndex } = action const { path, rowIndex } = action
const { remainingFields, rows } = separateRows(path, state) const { remainingFields, rows } = separateRows(path, state)
@@ -342,20 +205,100 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
return newState return newState
} }
case 'SET_ROW_COLLAPSED': { case 'REMOVE': {
const { path, updatedRows } = action const newState = { ...state }
if (newState[action.path]) {
delete newState[action.path]
}
return newState
}
const newState = { case 'REMOVE_ROW': {
...state, const { path, rowIndex } = action
const { remainingFields, rows } = separateRows(path, state)
const rowsMetadata = [...(state[path]?.rows || [])]
rows.splice(rowIndex, 1)
rowsMetadata.splice(rowIndex, 1)
const newState: FormState = {
...remainingFields,
[path]: { [path]: {
...state[path], ...state[path],
rows: updatedRows, disableFormData: rows.length > 0,
requiresRender: true,
rows: rowsMetadata,
value: rows.length,
},
...flattenRows(path, rows),
}
return newState
}
case 'REPLACE_ROW': {
const { blockType, path, rowIndex: rowIndexArg, subFieldState = {} } = action
const { remainingFields, rows: siblingRows } = separateRows(path, state)
const rowIndex = Math.max(0, Math.min(rowIndexArg, siblingRows?.length - 1 || 0))
const rowsMetadata = [...(state[path]?.rows || [])]
rowsMetadata[rowIndex] = {
id: new ObjectId().toHexString(),
blockType: blockType || undefined,
collapsed: false,
}
if (blockType) {
subFieldState.blockType = {
initialValue: blockType,
valid: true,
value: blockType,
}
}
// replace form _field state_
siblingRows[rowIndex] = subFieldState
const newState: FormState = {
...remainingFields,
...flattenRows(path, siblingRows),
[path]: {
...state[path],
disableFormData: true,
rows: rowsMetadata,
value: siblingRows.length,
}, },
} }
return newState return newState
} }
case 'REPLACE_STATE': {
if (action.optimize !== false) {
// Only update fields that have changed
// by comparing old value / initialValue to new
// ..
// This is a performance enhancement for saving
// large documents with hundreds of fields
const newState = {}
Object.entries(action.state).forEach(([path, field]) => {
const oldField = state[path]
const newField = field
if (!dequal(oldField, newField)) {
newState[path] = newField
} else if (oldField) {
newState[path] = oldField
}
})
return newState
}
// If we're not optimizing, just set the state to the new state
return action.state
}
case 'SET_ALL_ROWS_COLLAPSED': { case 'SET_ALL_ROWS_COLLAPSED': {
const { path, updatedRows } = action const { path, updatedRows } = action
@@ -368,6 +311,63 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
} }
} }
case 'SET_ROW_COLLAPSED': {
const { path, updatedRows } = action
const newState = {
...state,
[path]: {
...state[path],
rows: updatedRows,
},
}
return newState
}
case 'UPDATE': {
const newField = Object.entries(action).reduce(
(field, [key, value]) => {
if (
[
'disableFormData',
'errorMessage',
'initialValue',
'rows',
'valid',
'validate',
'value',
].includes(key)
) {
return {
...field,
[key]: value,
}
}
return field
},
state[action.path] || ({} as FormField),
)
const newState = {
...state,
[action.path]: newField,
}
return newState
}
case 'UPDATE_MANY': {
const newState = { ...state }
Object.entries(action.formState).forEach(([path, field]) => {
newState[path] = field
})
return newState
}
default: { default: {
return state return state
} }

View File

@@ -78,18 +78,18 @@ export function RenderField({
return <ArrayField {...sharedProps} field={clientFieldConfig} permissions={permissions} /> return <ArrayField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
case 'blocks': case 'blocks':
return <BlocksField {...sharedProps} field={clientFieldConfig} permissions={permissions} /> return <BlocksField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
case 'group':
return <GroupField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
case 'tabs':
return <TabsField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
// unnamed fields with subfields
case 'row':
return <RowField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
case 'collapsible': case 'collapsible':
return ( return (
<CollapsibleField {...sharedProps} field={clientFieldConfig} permissions={permissions} /> <CollapsibleField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
) )
case 'group':
return <GroupField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
// unnamed fields with subfields
case 'row':
return <RowField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
case 'tabs':
return <TabsField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
default: default:
return DefaultField ? <DefaultField field={clientFieldConfig} {...sharedProps} /> : null return DefaultField ? <DefaultField field={clientFieldConfig} {...sharedProps} /> : null

View File

@@ -473,8 +473,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
break break
} }
case 'upload': case 'relationship':
case 'relationship': { case 'upload': {
if (field.filterOptions) { if (field.filterOptions) {
if (typeof field.filterOptions === 'object') { if (typeof field.filterOptions === 'object') {
if (typeof field.relationTo === 'string') { if (typeof field.relationTo === 'string') {

View File

@@ -39,25 +39,6 @@ export const defaultValuePromise = async <T>({
// Traverse subfields // Traverse subfields
switch (field.type) { switch (field.type) {
case 'group': {
if (typeof siblingData[field.name] !== 'object') {
siblingData[field.name] = {}
}
const groupData = siblingData[field.name] as Record<string, unknown>
await iterateFields({
id,
data,
fields: field.fields,
locale,
siblingData: groupData,
user,
})
break
}
case 'array': { case 'array': {
const rows = siblingData[field.name] const rows = siblingData[field.name]
@@ -112,8 +93,9 @@ export const defaultValuePromise = async <T>({
break break
} }
case 'row': case 'collapsible':
case 'collapsible': {
case 'row': {
await iterateFields({ await iterateFields({
id, id,
data, data,
@@ -125,6 +107,24 @@ export const defaultValuePromise = async <T>({
break break
} }
case 'group': {
if (typeof siblingData[field.name] !== 'object') {
siblingData[field.name] = {}
}
const groupData = siblingData[field.name] as Record<string, unknown>
await iterateFields({
id,
data,
fields: field.fields,
locale,
siblingData: groupData,
user,
})
break
}
case 'tab': { case 'tab': {
let tabSiblingData let tabSiblingData

View File

@@ -33,8 +33,8 @@ export const traverseFields = ({
schemaMap.set(schemaPath, field) schemaMap.set(schemaPath, field)
switch (field.type) { switch (field.type) {
case 'group':
case 'array': case 'array':
case 'group':
traverseFields({ traverseFields({
config, config,
fields: field.fields, fields: field.fields,
@@ -46,19 +46,6 @@ export const traverseFields = ({
break break
case 'collapsible':
case 'row':
traverseFields({
config,
fields: field.fields,
i18n,
parentIndexPath: indexPath,
parentSchemaPath,
schemaMap,
})
break
case 'blocks': case 'blocks':
field.blocks.map((block) => { field.blocks.map((block) => {
const blockSchemaPath = `${schemaPath}.${block.slug}` const blockSchemaPath = `${schemaPath}.${block.slug}`
@@ -74,6 +61,19 @@ export const traverseFields = ({
}) })
}) })
break
case 'collapsible':
case 'row':
traverseFields({
config,
fields: field.fields,
i18n,
parentIndexPath: indexPath,
parentSchemaPath,
schemaMap,
})
break break
case 'richText': case 'richText':

886
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff