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'>
type InitNextResult =
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
| {
isSrcDir: boolean
nextAppDir: string
payloadConfigPath: string
success: true
}
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const { dbType: dbType, packageManager, projectDir } = args

View File

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

View File

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

View File

@@ -104,34 +104,10 @@ const traverseFields = ({
}
switch (field.type) {
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
case 'array':
case 'group':
case 'tab':
case 'array': {
case 'tab': {
let fieldSelect: SelectType
if (field.type === 'tab' && !tabHasName(field)) {
@@ -206,6 +182,30 @@ const traverseFields = ({
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:
break

View File

@@ -13,44 +13,6 @@ type Args = {
export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
fields.forEach((field) => {
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': {
const rowData = doc?.[field.name]
@@ -124,45 +86,47 @@ 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,
})
}
case 'collapsible':
// falls through
case 'row': {
return traverseFields({
doc,
fields: field.fields,
path,
rows,
})
}
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 'relationship':
// falls through
case 'upload': {
if (typeof field.relationTo === 'string') {
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) => {
args.fields.forEach((field) => {
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': {
const newTableName = args.adapter.tableNameMap.get(
`${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': {
return field.tabs.forEach((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) {
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': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -493,7 +316,6 @@ export const traverseFields = ({
break
}
case 'blocks': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -646,9 +468,82 @@ export const traverseFields = ({
break
}
case 'checkbox': {
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
break
}
case 'code':
case 'tab':
case 'group': {
case 'email':
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)) {
const {
hasLocalizedField: groupHasLocalizedField,
@@ -758,114 +653,136 @@ export const traverseFields = ({
break
}
case 'tabs': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
case 'json':
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,
})
case 'richText': {
targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
break
}
if (tabHasLocalizedField) {
hasLocalizedField = true
}
if (tabHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (tabHasManyTextField) {
hasManyTextField = true
}
if (tabHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (tabHasManyNumberField) {
hasManyNumberField = true
}
if (tabHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
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 'row':
case 'collapsible': {
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,
})
case 'point': {
break
}
case 'radio':
if (rowHasLocalizedField) {
hasLocalizedField = true
}
if (rowHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (rowHasManyTextField) {
hasManyTextField = true
}
if (rowHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (rowHasManyNumberField) {
hasManyNumberField = true
}
if (rowHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
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
}
@@ -931,6 +848,89 @@ export const traverseFields = ({
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:
break
}

View File

@@ -13,44 +13,6 @@ type Args = {
export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
fields.forEach((field) => {
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': {
const rowData = doc?.[field.name]
@@ -124,45 +86,47 @@ 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,
})
}
case 'collapsible':
// falls through
case 'row': {
return traverseFields({
doc,
fields: field.fields,
path,
rows,
})
}
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 'relationship':
// falls through
case 'upload': {
if (typeof field.relationTo === 'string') {
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) => {
args.fields.forEach((field) => {
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': {
const newTableName = args.adapter.tableNameMap.get(
`${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': {
return field.tabs.forEach((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
}
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': {
const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name]
@@ -356,6 +330,7 @@ export const traverseFields = ({
}
case 'group':
case 'tab': {
const fieldSelect = select?.[field.name]
@@ -389,47 +364,6 @@ export const traverseFields = ({
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': {
// when `joinsQuery` is false, do not join
if (joinQuery === false) {
@@ -621,6 +555,72 @@ export const traverseFields = ({
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: {
if (!select && !selectAllOnCurrentLevel) {
break

View File

@@ -182,197 +182,6 @@ export const traverseFields = ({
}
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': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -506,7 +315,6 @@ export const traverseFields = ({
break
}
case 'blocks': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
@@ -655,9 +463,88 @@ export const traverseFields = ({
break
}
case 'checkbox': {
targetTable[fieldName] = withDefault(boolean(columnName), field)
break
}
case 'code':
case 'tab':
case 'group': {
case 'email':
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)) {
const {
hasLocalizedField: groupHasLocalizedField,
@@ -765,112 +652,143 @@ export const traverseFields = ({
break
}
case 'tabs': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
case 'json':
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,
})
case 'richText': {
targetTable[fieldName] = withDefault(jsonb(columnName), field)
break
}
if (tabHasLocalizedField) {
hasLocalizedField = true
}
if (tabHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (tabHasManyTextField) {
hasManyTextField = true
}
if (tabHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (tabHasManyNumberField) {
hasManyNumberField = true
}
if (tabHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
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 'row':
case 'collapsible': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const {
hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
hasManyNumberField: rowHasManyNumberField,
hasManyTextField: rowHasManyTextField,
} = traverseFields({
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,
columnPrefix,
columns,
disableNotNull: disableNotNullFromHere,
disableUnique,
fieldPrefix,
fields: field.fields,
forceLocalized,
indexes,
localesColumns,
localesIndexes,
newTableName,
parentTableName,
relationships,
relationsToBuild,
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
})
if (rowHasLocalizedField) {
hasLocalizedField = true
}
if (rowHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = true
}
if (rowHasManyTextField) {
hasManyTextField = true
}
if (rowHasLocalizedManyTextField) {
hasLocalizedManyTextField = true
}
if (rowHasManyNumberField) {
hasManyNumberField = true
}
if (rowHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = true
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
}
@@ -936,6 +854,88 @@ export const traverseFields = ({
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:
break
}

View File

@@ -121,185 +121,6 @@ export const getTableColumnFromPath = ({
}
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': {
newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
@@ -341,7 +162,6 @@ export const getTableColumnFromPath = ({
value,
})
}
case 'blocks': {
let blockTableColumn: TableColumn
let newTableName: string
@@ -447,7 +267,87 @@ export const getTableColumnFromPath = ({
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 'upload': {
const newCollectionPath = pathSegments.slice(1).join('.')
if (Array.isArray(field.relationTo) || field.hasMany) {
@@ -692,6 +592,106 @@ export const getTableColumnFromPath = ({
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: {
// fall through
break

View File

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

View File

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

View File

@@ -17,23 +17,23 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@eslint-react/eslint-plugin": "1.12.3",
"@eslint/js": "9.9.1",
"@eslint-react/eslint-plugin": "1.16.1",
"@eslint/js": "9.14.0",
"@payloadcms/eslint-plugin": "workspace:*",
"@types/eslint": "9.6.1",
"@types/eslint__js": "8.42.3",
"@typescript-eslint/parser": "8.3.0",
"eslint": "9.9.1",
"@typescript-eslint/parser": "8.14.0",
"eslint": "9.14.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import-x": "4.1.1",
"eslint-plugin-jest": "28.8.1",
"eslint-plugin-import-x": "4.4.2",
"eslint-plugin-jest": "28.9.0",
"eslint-plugin-jest-dom": "5.4.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-perfectionist": "3.3.0",
"eslint-plugin-react-hooks": "5.1.0-rc-a19a8ab4-20240829",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.9.1",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-regexp": "2.6.0",
"globals": "15.9.0",
"globals": "15.12.0",
"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"
},
"dependencies": {
"@eslint-react/eslint-plugin": "1.12.3",
"@eslint/js": "9.9.1",
"@eslint-react/eslint-plugin": "1.16.1",
"@eslint/js": "9.14.0",
"@types/eslint": "9.6.1",
"@types/eslint__js": "8.42.3",
"@typescript-eslint/parser": "8.3.0",
"eslint": "9.9.1",
"@typescript-eslint/parser": "8.14.0",
"eslint": "9.14.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import-x": "4.1.1",
"eslint-plugin-jest": "28.8.1",
"eslint-plugin-import-x": "4.4.2",
"eslint-plugin-jest": "28.9.0",
"eslint-plugin-jest-dom": "5.4.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-perfectionist": "3.3.0",
"eslint-plugin-react-hooks": "5.1.0-rc-a19a8ab4-20240829",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.9.1",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-regexp": "2.6.0",
"globals": "15.9.0",
"globals": "15.12.0",
"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
switch (operation.operation) {
case 'query':
this.complexity += this.nodeComplexity(operation, this.context.getSchema().getQueryType())
break
case 'mutation':
this.complexity += this.nodeComplexity(
operation,
this.context.getSchema().getMutationType(),
)
break
case 'query':
this.complexity += this.nodeComplexity(operation, this.context.getSchema().getQueryType())
break
case 'subscription':
this.complexity += this.nodeComplexity(
operation,

View File

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

View File

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

View File

@@ -106,7 +106,7 @@
"babel-plugin-react-compiler": "0.0.0-experimental-24ec0eb-20240918",
"esbuild": "0.23.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:*",
"swc-plugin-transform-remove-imports": "1.15.0"
},

View File

@@ -1,5 +1,5 @@
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 => {
return Boolean(

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,21 +65,6 @@ export const migrate = async ({ config, parsedArgs }: Args): Promise<void> => {
case 'migrate':
await adapter.migrate()
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':
try {
await adapter.createMigration({
@@ -92,6 +77,21 @@ export const migrate = async ({ config, parsedArgs }: Args): Promise<void> => {
throw new Error(`Error creating migration: ${err.message}`)
}
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:
payload.logger.error({

View File

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

View File

@@ -111,8 +111,8 @@ export const createClientField = ({
switch (incomingField.type) {
case 'array':
case 'group':
case 'collapsible':
case 'group':
case 'row': {
const field = clientField as unknown as RowFieldClient
@@ -182,6 +182,31 @@ export const createClientField = ({
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': {
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
@@ -193,7 +218,6 @@ export const createClientField = ({
break
}
case 'tabs': {
const field = clientField as unknown as TabsFieldClient
@@ -221,30 +245,6 @@ export const createClientField = ({
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:
break
}

View File

@@ -97,27 +97,6 @@ export const promise = async ({
// Traverse subfields
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': {
const rows = siblingDoc[field.name]
@@ -185,8 +164,9 @@ export const promise = async ({
break
}
case 'row':
case 'collapsible': {
case 'collapsible':
case 'row': {
await traverseFields({
collection,
context,
@@ -206,6 +186,66 @@ export const promise = async ({
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': {
let tabSiblingData = siblingData
@@ -258,46 +298,6 @@ export const promise = async ({
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: {
break
}

View File

@@ -181,15 +181,13 @@ export const promise = async ({
break
}
case 'tabs': {
field.tabs.forEach((tab) => {
if (
tabHasName(tab) &&
(typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null)
) {
siblingDoc[tab.name] = {}
}
})
case 'point': {
const pointDoc = siblingDoc[field.name] as Record<string, unknown>
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
siblingDoc[field.name] = pointDoc.coordinates
} else {
siblingDoc[field.name] = undefined
}
break
}
@@ -206,13 +204,15 @@ export const promise = async ({
break
}
case 'point': {
const pointDoc = siblingDoc[field.name] as Record<string, unknown>
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
siblingDoc[field.name] = pointDoc.coordinates
} else {
siblingDoc[field.name] = undefined
}
case 'tabs': {
field.tabs.forEach((tab) => {
if (
tabHasName(tab) &&
(typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null)
) {
siblingDoc[tab.name] = {}
}
})
break
}
@@ -347,45 +347,6 @@ export const promise = async ({
}
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': {
const rows = siblingDoc[field.name] as JsonObject
@@ -573,8 +534,9 @@ export const promise = async ({
break
}
case 'row':
case 'collapsible': {
case 'collapsible':
case 'row': {
traverseFields({
collection,
context,
@@ -605,23 +567,14 @@ export const promise = async ({
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
case 'group': {
let groupDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') {
groupDoc = {}
}
const groupSelect = select?.[field.name]
traverseFields({
collection,
context,
@@ -642,10 +595,10 @@ export const promise = async ({
populationPromises,
req,
schemaPath: fieldSchemaPath,
select: tabSelect,
select: typeof groupSelect === 'object' ? groupSelect : undefined,
selectMode,
showHiddenFields,
siblingDoc: tabDoc,
siblingDoc: groupDoc,
triggerAccessControl,
triggerHooks,
})
@@ -653,37 +606,6 @@ export const promise = async ({
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': {
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
@@ -781,6 +703,84 @@ export const promise = async ({
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: {
break
}

View File

@@ -2,9 +2,9 @@ import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { ValidationFieldError } from '../../../errors/index.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 { Field, TabAsField } from '../../config/types.js'
import type { RequestContext } from '../../../index.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
@@ -200,60 +200,6 @@ export const promise = async ({
}
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': {
const rows = siblingData[field.name]
@@ -339,8 +285,9 @@ export const promise = async ({
break
}
case 'row':
case 'collapsible': {
case 'collapsible':
case 'row': {
await traverseFields({
id,
collection,
@@ -365,6 +312,104 @@ export const promise = async ({
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': {
let tabSiblingData = siblingData
let tabSiblingDoc = siblingDoc
@@ -435,51 +480,6 @@ export const promise = async ({
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: {
break
}

View File

@@ -145,26 +145,6 @@ export const promise = async <T>({
localization.localeCodes.forEach((locale) => {
if (fieldData[locale]) {
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': {
const rows = fieldData[locale]
@@ -189,7 +169,6 @@ export const promise = async <T>({
}
break
}
case 'blocks': {
const rows = fieldData[locale]
@@ -220,6 +199,27 @@ export const promise = async <T>({
}
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
// so the child fields can run beforeDuplicate hooks
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': {
const rows = siblingDoc[field.name]
@@ -279,7 +255,6 @@ export const promise = async <T>({
}
break
}
case 'blocks': {
const rows = siblingDoc[field.name]
@@ -313,13 +288,38 @@ export const promise = async <T>({
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 {
// Finally, we traverse fields which do not affect data here
switch (field.type) {
case 'row':
case 'collapsible': {
case 'collapsible':
case 'row': {
await traverseFields({
id,
collection,

View File

@@ -90,6 +90,31 @@ export const promise = async <T>({
// Sanitize incoming data
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': {
if (typeof siblingData[field.name] === 'string') {
const value = siblingData[field.name] as string
@@ -114,36 +139,8 @@ export const promise = async <T>({
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 'upload': {
if (
siblingData[field.name] === '' ||
@@ -230,12 +227,15 @@ export const promise = async <T>({
}
break
}
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] = []
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
@@ -305,37 +305,6 @@ export const promise = async <T>({
// Traverse subfields
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': {
const rows = siblingData[field.name]
@@ -405,8 +374,9 @@ export const promise = async <T>({
break
}
case 'row':
case 'collapsible': {
case 'collapsible':
case 'row': {
await traverseFields({
id,
collection,
@@ -426,6 +396,76 @@ export const promise = async <T>({
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': {
let tabSiblingData
@@ -486,46 +526,6 @@ export const promise = async <T>({
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: {
break
}

View File

@@ -239,123 +239,125 @@ export function fieldsToJSONSchema(
let fieldSchema: JSONSchema4
switch (field.type) {
case 'text':
if (field.hasMany === true) {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: { type: 'string' },
}
} else {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
case 'array': {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: {
type: 'object',
additionalProperties: false,
...fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
),
},
}
break
case 'textarea':
case 'code':
case 'email':
case 'date': {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
break
}
case 'number': {
if (field.hasMany === true) {
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: { type: 'number' },
$ref: `#/definitions/${field.interfaceName}`,
}
} else {
fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }
}
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)
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: hasBlocks
? {
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 (block.interfaceName) {
interfaceNameDefinitions.set(block.interfaceName, blockSchema)
return {
$ref: `#/definitions/${block.interfaceName}`,
}
}
return blockSchema
}),
}
: {},
}
break
}
case 'checkbox': {
fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }
break
}
case 'code':
case 'date':
case 'json': {
fieldSchema = field.jsonSchema?.schema || {
type: ['object', 'array', 'string', 'number', 'boolean', 'null'],
}
case 'email':
case 'textarea': {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
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({
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 = {
type: 'object',
additionalProperties: false,
...fieldsToJSONSchema(
collectionIDFieldTypes,
config,
field,
field.fields,
interfaceNameDefinitions,
isRequired,
})
} else {
// Maintain backwards compatibility with existing rich text editors
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: {
type: 'object',
},
}
config,
),
}
break
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
case 'radio': {
fieldSchema = {
type: withNullableJSONSchemaType('string', isRequired),
enum: buildOptionEnums(field.options),
}
break
}
case 'select': {
const optionEnums = buildOptionEnums(field.options)
if (field.hasMany) {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: {
type: 'string',
},
$ref: `#/definitions/${field.interfaceName}`,
}
if (optionEnums?.length) {
;(fieldSchema.items as JSONSchema4).enum = optionEnums
}
} else {
fieldSchema = {
type: withNullableJSONSchemaType('string', isRequired),
}
if (optionEnums?.length) {
fieldSchema.enum = optionEnums
}
}
break
}
case 'point': {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: [
{
type: 'number',
},
{
type: 'number',
},
],
maxItems: 2,
minItems: 2,
}
break
}
@@ -384,8 +386,53 @@ export function fieldsToJSONSchema(
break
}
case 'upload':
case 'relationship': {
case 'json': {
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 (field.hasMany) {
fieldSchema = {
@@ -474,91 +521,55 @@ export function fieldsToJSONSchema(
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)
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: hasBlocks
? {
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 (block.interfaceName) {
interfaceNameDefinitions.set(block.interfaceName, blockSchema)
return {
$ref: `#/definitions/${block.interfaceName}`,
}
}
return blockSchema
}),
}
: {},
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
}
break
}
case 'array': {
fieldSchema = {
type: withNullableJSONSchemaType('array', isRequired),
items: {
type: 'object',
additionalProperties: false,
...fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
),
},
if (typeof field.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
if (field.editor.outputSchema) {
fieldSchema = field.editor.outputSchema({
collectionIDFieldTypes,
config,
field,
interfaceNameDefinitions,
isRequired,
})
} else {
// Maintain backwards compatibility with existing rich text editors
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
type: withNullableJSONSchemaType('array', isRequired),
items: {
type: 'object',
},
}
}
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
}
@@ -596,27 +607,16 @@ export function fieldsToJSONSchema(
break
}
case 'group': {
fieldSchema = {
type: 'object',
additionalProperties: false,
...fieldsToJSONSchema(
collectionIDFieldTypes,
field.fields,
interfaceNameDefinitions,
config,
),
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
case 'text':
if (field.hasMany === true) {
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
type: withNullableJSONSchemaType('array', isRequired),
items: { type: 'string' },
}
} else {
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
}
break
}
default: {
break
@@ -704,15 +704,6 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS
for (const field of fields) {
switch (field.type) {
case 'row':
case 'collapsible':
schema.properties = {
...schema.properties,
...fieldsToSelectJSONSchema({ fields: field.fields }).properties,
}
break
case 'array':
case 'group':
schema.properties[field.name] = {
@@ -725,27 +716,6 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS
}
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': {
const blocksSchema: JSONSchema4 = {
type: 'object',
@@ -775,6 +745,36 @@ export function fieldsToSelectJSONSchema({ fields }: { fields: Field[] }): JSONS
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:
schema.properties[field.name] = {
@@ -800,6 +800,34 @@ const generateAuthFieldTypes = ({
}): JSONSchema4 => {
if (loginWithUsername) {
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': {
if (loginWithUsername.allowEmailLogin) {
// allow username or email and require password for login
@@ -858,34 +886,6 @@ const generateAuthFieldTypes = ({
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
switch (field.type) {
case 'group':
acc.push({
name: field.name,
type: field.type,
fields: fieldSchemaToJSON(field.fields),
})
break
case 'array':
acc.push({
name: field.name,
@@ -61,11 +52,31 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
break
case 'row':
case 'collapsible':
case 'row':
result = result.concat(fieldSchemaToJSON(field.fields))
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': {
let tabFields = []
@@ -87,17 +98,6 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
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:
if ('name' in field) {
acc.push({

View File

@@ -21,7 +21,7 @@ import { envPaths } from './envPaths.js'
const createPlainObject = <T = Record<string, unknown>>(): T => Object.create(null)
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

View File

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

View File

@@ -91,6 +91,30 @@ export const serializeSlate = (children?: Node[], submissionData?: any): string
<h6>
${serializeSlate(node.children, submissionData)}
</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':
return `
@@ -104,30 +128,6 @@ export const serializeSlate = (children?: Node[], submissionData?: any): string
${serializeSlate(node.children, submissionData)}
</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:
return `

View File

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

View File

@@ -93,7 +93,7 @@
"babel-plugin-transform-remove-imports": "^1.8.0",
"esbuild": "0.23.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:*",
"swc-plugin-transform-remove-imports": "1.15.0"
},

View File

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

View File

@@ -84,7 +84,7 @@ export const ToolbarButton = ({
className={className}
onClick={() => {
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(() => {
// 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}
onClick={() => {
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(() => {
// 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.type === 'relationship' || element.type === 'upload') {
switch (element.textAlign) {
case 'center':
attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
break
case 'left':
attr = { ...attr, style: { marginRight: 'auto' } }
break
case 'right':
attr = { ...attr, style: { marginLeft: 'auto' } }
break
case 'center':
attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
break
default:
attr = { ...attr, style: { textAlign: element.textAlign } }
break
}
} else if (element.type === 'li') {
switch (element.textAlign) {
case 'right':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
break
case 'center':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'center' } }
break
case 'right':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
break
case 'left':
default:
attr = { ...attr, style: { listStylePosition: 'outside', textAlign: 'left' } }

View File

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

View File

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

View File

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

View File

@@ -122,7 +122,7 @@
"babel-plugin-react-compiler": "0.0.0-experimental-24ec0eb-20240918",
"esbuild": "0.23.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:*"
},
"peerDependencies": {

View File

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

View File

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

View File

@@ -35,18 +35,6 @@ type Action =
export function formsManagementReducer(state: State, action: Action): State {
switch (action.type) {
case 'REPLACE': {
return {
...state,
...action.state,
}
}
case 'SET_ACTIVE_INDEX': {
return {
...state,
activeIndex: action.index,
}
}
case 'ADD_FORMS': {
const newForms: State['forms'] = []
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,
}
}
case 'REPLACE': {
return {
...state,
...action.state,
}
}
case 'SET_ACTIVE_INDEX': {
return {
...state,
activeIndex: action.index,
}
}
case 'UPDATE_ERROR_COUNT': {
const forms = [...state.forms]
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 { toast } from 'sonner'
import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js'
import { useForm, useFormModified } from '../../forms/Form/context.js'
import { useConfig } from '../../providers/Config/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 { drawerZBase } from '../Drawer/index.js'
import { PopupList } from '../Popup/index.js'
import { DocumentDrawerContextType } from '../DocumentDrawer/Provider.jsx'
import './index.scss'
const baseClass = 'duplicate'

View File

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

View File

@@ -1,18 +1,16 @@
'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 { getTranslation } from '@payloadcms/translations'
import LinkImport from 'next/link.js'
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 { useTranslation } from '../../../providers/Translation/index.js'
import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
import { CodeCell } from './fields/Code/index.js'
import { cellComponents } from './fields/index.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
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 {
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': {
const newState = { ...state }
if (typeof newState[action.relationTo] !== 'object') {
@@ -52,6 +39,19 @@ export function reducer(state: Documents, action: Action): Documents {
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: {
return state
}

View File

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

View File

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

View File

@@ -28,61 +28,6 @@ const sortOptions = (options: Option[]): Option[] =>
export const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] => {
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': {
const { collection, config, docs, i18n, ids = [], sort } = action
const relation = collection.slug
@@ -146,6 +91,35 @@ export const optionsReducer = (state: OptionGroup[], action: Action): OptionGrou
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': {
const { id, collection } = action
@@ -167,6 +141,32 @@ export const optionsReducer = (state: OptionGroup[], action: Action): OptionGrou
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: {
return state
}

View File

@@ -17,36 +17,53 @@ const ObjectId = (ObjectIdImport.default ||
*/
export function fieldReducer(state: FormState, action: FieldAction): FormState {
switch (action.type) {
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 = {}
case 'ADD_ROW': {
const { blockType, path, rowIndex: rowIndexFromArgs, subFieldState = {} } = action
Object.entries(action.state).forEach(([path, field]) => {
const oldField = state[path]
const newField = field
const rowIndex =
typeof rowIndexFromArgs === 'number' ? rowIndexFromArgs : state[path]?.rows?.length || 0
if (!dequal(oldField, newField)) {
newState[path] = newField
} else if (oldField) {
newState[path] = oldField
}
})
return newState
const withNewRow = [...(state[path]?.rows || [])]
const newRow: Row = {
id: (subFieldState?.id?.value as string) || new ObjectId().toHexString(),
blockType: blockType || undefined,
collapsed: false,
}
// If we're not optimizing, just set the state to the new state
return action.state
}
case 'REMOVE': {
const newState = { ...state }
if (newState[action.path]) {
delete newState[action.path]
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
}
@@ -112,160 +129,6 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
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': {
const { path, rowIndex } = action
const { remainingFields, rows } = separateRows(path, state)
@@ -342,20 +205,100 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
return newState
}
case 'SET_ROW_COLLAPSED': {
const { path, updatedRows } = action
case 'REMOVE': {
const newState = { ...state }
if (newState[action.path]) {
delete newState[action.path]
}
return newState
}
const newState = {
...state,
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],
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
}
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': {
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: {
return state
}

View File

@@ -78,18 +78,18 @@ export function RenderField({
return <ArrayField {...sharedProps} field={clientFieldConfig} permissions={permissions} />
case 'blocks':
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':
return (
<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:
return DefaultField ? <DefaultField field={clientFieldConfig} {...sharedProps} /> : null

View File

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

View File

@@ -39,25 +39,6 @@ export const defaultValuePromise = async <T>({
// Traverse subfields
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': {
const rows = siblingData[field.name]
@@ -112,8 +93,9 @@ export const defaultValuePromise = async <T>({
break
}
case 'row':
case 'collapsible': {
case 'collapsible':
case 'row': {
await iterateFields({
id,
data,
@@ -125,6 +107,24 @@ export const defaultValuePromise = async <T>({
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': {
let tabSiblingData

View File

@@ -33,8 +33,8 @@ export const traverseFields = ({
schemaMap.set(schemaPath, field)
switch (field.type) {
case 'group':
case 'array':
case 'group':
traverseFields({
config,
fields: field.fields,
@@ -46,19 +46,6 @@ export const traverseFields = ({
break
case 'collapsible':
case 'row':
traverseFields({
config,
fields: field.fields,
i18n,
parentIndexPath: indexPath,
parentSchemaPath,
schemaMap,
})
break
case 'blocks':
field.blocks.map((block) => {
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
case 'richText':

886
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff