feat: upload hasmany (#7796)

Supports `hasMany` upload fields, similar to how `hasMany` works in
other fields, i.e.:

```ts
{
  type: 'upload',
  relationTo: 'media',
  hasMany: true
}
```

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
This commit is contained in:
Paul
2024-08-21 18:44:04 -06:00
committed by GitHub
parent a687cb9c5b
commit d2571e10d6
57 changed files with 1624 additions and 432 deletions

View File

@@ -595,14 +595,77 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: mongoose.Schema.Types.Mixed,
ref: field.relationTo,
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
if (field.localized && config.localization) {
schemaToReturn = {
type: config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
type: mongoose.Schema.Types.Mixed,
refPath: `${field.name}.${locale}.relationTo`,
},
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: mongoose.Schema.Types.Mixed,
ref: field.relationTo,
}
}
return {
...locales,
[locale]: field.hasMany
? { type: [localeSchema], default: formatDefaultValue(field) }
: localeSchema,
}
}, {}),
localized: true,
}
} else if (hasManyRelations) {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
value: {
type: mongoose.Schema.Types.Mixed,
refPath: `${field.name}.relationTo`,
},
}
if (field.hasMany) {
schemaToReturn = {
type: [schemaToReturn],
default: formatDefaultValue(field),
}
}
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
type: mongoose.Schema.Types.Mixed,
ref: field.relationTo,
}
if (field.hasMany) {
schemaToReturn = {
type: [schemaToReturn],
default: formatDefaultValue(field),
}
}
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization),
[field.name]: schemaToReturn,
})
},
}

View File

@@ -717,7 +717,7 @@ export const traverseFields = ({
case 'upload':
if (Array.isArray(field.relationTo)) {
field.relationTo.forEach((relation) => relationships.add(relation))
} else if (field.type === 'relationship' && field.hasMany) {
} else if (field.hasMany) {
relationships.add(field.relationTo)
} else {
// simple relationships get a column on the targetTable with a foreign key to the relationTo table

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type { Field } from 'payload'
import { fieldAffectsData, tabHasName } from 'payload/shared'
@@ -34,8 +33,9 @@ export const traverseFields = ({
// handle simple relationship
if (
depth > 0 &&
(field.type === 'upload' ||
(field.type === 'relationship' && !field.hasMany && typeof field.relationTo === 'string'))
(field.type === 'upload' || field.type === 'relationship') &&
!field.hasMany &&
typeof field.relationTo === 'string'
) {
if (field.localized) {
_locales.with[`${path}${field.name}`] = true

View File

@@ -726,7 +726,7 @@ export const traverseFields = ({
case 'upload':
if (Array.isArray(field.relationTo)) {
field.relationTo.forEach((relation) => relationships.add(relation))
} else if (field.type === 'relationship' && field.hasMany) {
} else if (field.hasMany) {
relationships.add(field.relationTo)
} else {
// simple relationships get a column on the targetTable with a foreign key to the relationTo table

View File

@@ -445,7 +445,7 @@ export const getTableColumnFromPath = ({
case 'relationship':
case 'upload': {
const newCollectionPath = pathSegments.slice(1).join('.')
if (Array.isArray(field.relationTo) || (field.type === 'relationship' && field.hasMany)) {
if (Array.isArray(field.relationTo) || field.hasMany) {
let relationshipFields
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
const {

View File

@@ -307,10 +307,51 @@ export function buildMutationInputType({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
}),
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => {
const { relationTo } = field
type PayloadGraphQLRelationshipType =
| GraphQLInputObjectType
| GraphQLList<GraphQLScalarType>
| GraphQLScalarType
let type: PayloadGraphQLRelationshipType
if (Array.isArray(relationTo)) {
const fullName = `${combineParentName(
parentName,
toWords(field.name, true),
)}RelationshipInput`
type = new GraphQLInputObjectType({
name: fullName,
fields: {
relationTo: {
type: new GraphQLEnumType({
name: `${fullName}RelationTo`,
values: relationTo.reduce(
(values, option) => ({
...values,
[formatName(option)]: {
value: option,
},
}),
{},
),
}),
},
value: { type: GraphQLJSON },
},
})
} else {
type = getCollectionIDType(
config.db.defaultIDType,
graphqlResult.collections[relationTo].config,
)
}
return {
...inputObjectTypeConfig,
[field.name]: { type: field.hasMany ? new GraphQLList(type) : type },
}
},
}
const fieldName = formatName(name)

View File

@@ -594,49 +594,164 @@ export function buildObjectType({
}),
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
const { relationTo } = field
const isRelatedToManyCollections = Array.isArray(relationTo)
const hasManyValues = field.hasMany
const relationshipName = combineParentName(parentName, toWords(field.name, true))
const uploadName = combineParentName(parentName, toWords(field.name, true))
let type
let relationToType = null
if (Array.isArray(relationTo)) {
relationToType = new GraphQLEnumType({
name: `${relationshipName}_RelationTo`,
values: relationTo.reduce(
(relations, relation) => ({
...relations,
[formatName(relation)]: {
value: relation,
},
}),
{},
),
})
const types = relationTo.map((relation) => graphqlResult.collections[relation].graphQL.type)
type = new GraphQLObjectType({
name: `${relationshipName}_Relationship`,
fields: {
relationTo: {
type: relationToType,
},
value: {
type: new GraphQLUnionType({
name: relationshipName,
resolveType(data, { req }) {
return graphqlResult.collections[data.collection].graphQL.type.name
},
types,
}),
},
},
})
} else {
;({ type } = graphqlResult.collections[relationTo].graphQL)
}
// If the relationshipType is undefined at this point,
// it can be assumed that this blockType can have a relationship
// to itself. Therefore, we set the relationshipType equal to the blockType
// that is currently being created.
const type = withNullableType(
field,
graphqlResult.collections[relationTo].graphQL.type || newlyCreatedBlockType,
forceNullable,
type = type || newlyCreatedBlockType
const relationshipArgs: {
draft?: unknown
fallbackLocale?: unknown
limit?: unknown
locale?: unknown
page?: unknown
where?: unknown
} = {}
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo]).some(
(relation) => graphqlResult.collections[relation].config.versions?.drafts,
)
const uploadArgs = {} as LocaleInputType
if (relationsUseDrafts) {
relationshipArgs.draft = {
type: GraphQLBoolean,
}
}
if (config.localization) {
uploadArgs.locale = {
relationshipArgs.locale = {
type: graphqlResult.types.localeInputType,
}
uploadArgs.fallbackLocale = {
relationshipArgs.fallbackLocale = {
type: graphqlResult.types.fallbackLocaleInputType,
}
}
const relatedCollectionSlug = field.relationTo
const upload = {
type,
args: uploadArgs,
extensions: { complexity: 20 },
const relationship = {
type: withNullableType(
field,
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
forceNullable,
),
args: relationshipArgs,
extensions: { complexity: 10 },
async resolve(parent, args, context: Context) {
const value = parent[field.name]
const locale = args.locale || context.req.locale
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
const id = value
let relatedCollectionSlug = field.relationTo
const draft = Boolean(args.draft ?? context.req.query?.draft)
if (hasManyValues) {
const results = []
const resultPromises = []
const createPopulationPromise = async (relatedDoc, i) => {
let id = relatedDoc
let collectionSlug = field.relationTo
if (isRelatedToManyCollections) {
collectionSlug = relatedDoc.relationTo
id = relatedDoc.value
}
const result = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: collectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)
if (result) {
if (isRelatedToManyCollections) {
results[i] = {
relationTo: collectionSlug,
value: {
...result,
collection: collectionSlug,
},
}
} else {
results[i] = result
}
}
}
if (value) {
value.forEach((relatedDoc, i) => {
resultPromises.push(createPopulationPromise(relatedDoc, i))
})
}
await Promise.all(resultPromises)
return results
}
let id = value
if (isRelatedToManyCollections && value) {
id = value.value
relatedCollectionSlug = value.relationTo
}
if (id) {
const relatedDocument = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: relatedCollectionSlug,
collectionSlug: relatedCollectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
@@ -649,26 +764,30 @@ export function buildObjectType({
}),
)
return relatedDocument || null
if (relatedDocument) {
if (isRelatedToManyCollections) {
return {
relationTo: relatedCollectionSlug,
value: {
...relatedDocument,
collection: relatedCollectionSlug,
},
}
}
return relatedDocument
}
return null
}
return null
},
}
const whereFields = graphqlResult.collections[relationTo].config.fields
upload.args.where = {
type: buildWhereInputType({
name: uploadName,
fields: whereFields,
parentName: uploadName,
}),
}
return {
...objectTypeConfig,
[field.name]: upload,
[field.name]: relationship,
}
},
}

View File

@@ -130,9 +130,36 @@ const fieldToSchemaMap = ({ nestedFieldName, parentName }: Args): any => ({
textarea: (field: TextareaField) => ({
type: withOperators(field, parentName),
}),
upload: (field: UploadField) => ({
type: withOperators(field, parentName),
}),
upload: (field: UploadField) => {
if (Array.isArray(field.relationTo)) {
return {
type: new GraphQLInputObjectType({
name: `${combineParentName(parentName, field.name)}_Relation`,
fields: {
relationTo: {
type: new GraphQLEnumType({
name: `${combineParentName(parentName, field.name)}_Relation_RelationTo`,
values: field.relationTo.reduce(
(values, relation) => ({
...values,
[formatName(relation)]: {
value: relation,
},
}),
{},
),
}),
},
value: { type: GraphQLJSON },
},
}),
}
}
return {
type: withOperators(field, parentName),
}
},
})
export default fieldToSchemaMap

View File

@@ -230,9 +230,9 @@ const defaults: DefaultsType = {
},
upload: {
operators: [
...operators.equality.map((operator) => ({
...[...operators.equality, ...operators.contains].map((operator) => ({
name: operator,
type: GraphQLString,
type: GraphQLJSON,
})),
],
},

View File

@@ -46,7 +46,16 @@ const baseClass = 'collection-list'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const DefaultListView: React.FC = () => {
const { Header, collectionSlug, hasCreatePermission, newDocumentURL } = useListInfo()
const {
Header,
beforeActions,
collectionSlug,
disableBulkDelete,
disableBulkEdit,
hasCreatePermission,
newDocumentURL,
} = useListInfo()
const { data, defaultLimit, handlePageChange, handlePerPageChange } = useListQuery()
const { searchParams } = useSearchParams()
const { openModal } = useModal()
@@ -221,10 +230,15 @@ export const DefaultListView: React.FC = () => {
<div className={`${baseClass}__list-selection`}>
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
<div className={`${baseClass}__list-selection-actions`}>
<EditMany collection={collectionConfig} fields={fields} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
<DeleteMany collection={collectionConfig} />
{beforeActions && beforeActions}
{!disableBulkEdit && (
<Fragment>
<EditMany collection={collectionConfig} fields={fields} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
</Fragment>
)}
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
</div>
</div>
)}

View File

@@ -105,7 +105,10 @@ export async function validateSearchParam({
fieldPath = path.slice(0, -(req.locale.length + 1))
}
// remove ".value" from ends of polymorphic relationship paths
if (field.type === 'relationship' && Array.isArray(field.relationTo)) {
if (
(field.type === 'relationship' || field.type === 'upload') &&
Array.isArray(field.relationTo)
) {
fieldPath = fieldPath.replace('.value', '')
}
const entityType: 'collections' | 'globals' = globalConfig ? 'globals' : 'collections'

View File

@@ -99,19 +99,26 @@ export const sanitizeFields = async ({
})
}
if (field.type === 'relationship') {
if (field.min && !field.minRows) {
console.warn(
`(payload): The "min" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "minRows" instead.`,
)
if (field.min && !field.minRows) {
console.warn(
`(payload): The "min" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "minRows" instead.`,
)
}
if (field.max && !field.maxRows) {
console.warn(
`(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`,
)
}
field.minRows = field.minRows || field.min
field.maxRows = field.maxRows || field.max
}
if (field.type === 'upload') {
if (!field.admin || !('isSortable' in field.admin)) {
field.admin = {
isSortable: true,
...field.admin,
}
if (field.max && !field.maxRows) {
console.warn(
`(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`,
)
}
field.minRows = field.minRows || field.min
field.maxRows = field.maxRows || field.max
}
}

View File

@@ -824,35 +824,106 @@ export type UIFieldClient = {
} & Omit<DeepUndefinable<FieldBaseClient>, '_isPresentational' | 'admin'> & // still include FieldBaseClient (even if it's undefinable) so that we don't need constant type checks (e.g. if('xy' in field))
Pick<UIField, 'label' | 'name' | 'type'>
export type UploadField = {
admin?: {
components?: {
Error?: CustomComponent<UploadFieldErrorClientComponent | UploadFieldErrorServerComponent>
Label?: CustomComponent<UploadFieldLabelClientComponent | UploadFieldLabelServerComponent>
} & Admin['components']
}
type SharedUploadProperties = {
/**
* Toggle the preview in the admin interface.
*/
displayPreview?: boolean
filterOptions?: FilterOptions
hasMany?: boolean
/**
* Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached.
*
* {@link https://payloadcms.com/docs/getting-started/concepts#field-level-max-depth}
*/
maxDepth?: number
relationTo: CollectionSlug
type: 'upload'
validate?: Validate<unknown, unknown, unknown, UploadField>
} & FieldBase
validate?: Validate<unknown, unknown, unknown, SharedUploadProperties>
} & (
| {
hasMany: true
/**
* @deprecated Use 'maxRows' instead
*/
max?: number
maxRows?: number
/**
* @deprecated Use 'minRows' instead
*/
min?: number
minRows?: number
}
| {
hasMany?: false | undefined
/**
* @deprecated Use 'maxRows' instead
*/
max?: undefined
maxRows?: undefined
/**
* @deprecated Use 'minRows' instead
*/
min?: undefined
minRows?: undefined
}
) &
FieldBase
export type UploadFieldClient = {
type SharedUploadPropertiesClient = FieldBaseClient &
Pick<
SharedUploadProperties,
'hasMany' | 'max' | 'maxDepth' | 'maxRows' | 'min' | 'minRows' | 'type'
>
type UploadAdmin = {
allowCreate?: boolean
components?: {
Error?: CustomComponent<
RelationshipFieldErrorClientComponent | RelationshipFieldErrorServerComponent
>
Label?: CustomComponent<
RelationshipFieldLabelClientComponent | RelationshipFieldLabelServerComponent
>
} & Admin['components']
isSortable?: boolean
} & Admin
type UploadAdminClient = {
components?: {
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
export type PolymorphicUploadField = {
admin?: {
components?: {
Error?: MappedComponent
Label?: MappedComponent
} & AdminClient['components']
}
} & FieldBaseClient &
Pick<UploadField, 'displayPreview' | 'maxDepth' | 'relationTo' | 'type'>
sortOptions?: { [collectionSlug: CollectionSlug]: string }
} & UploadAdmin
relationTo: CollectionSlug[]
} & SharedUploadProperties
export type PolymorphicUploadFieldClient = {
admin?: {
sortOptions?: Pick<PolymorphicUploadField['admin'], 'sortOptions'>
} & UploadAdminClient
} & Pick<PolymorphicUploadField, 'displayPreview' | 'maxDepth' | 'relationTo' | 'type'> &
SharedUploadPropertiesClient
export type SingleUploadField = {
admin?: {
sortOptions?: string
} & UploadAdmin
relationTo: CollectionSlug
} & SharedUploadProperties
export type SingleUploadFieldClient = {
admin?: Pick<SingleUploadField['admin'], 'sortOptions'> & UploadAdminClient
} & Pick<SingleUploadField, 'displayPreview' | 'maxDepth' | 'relationTo' | 'type'> &
SharedUploadPropertiesClient
export type UploadField = /* PolymorphicUploadField | */ SingleUploadField
export type UploadFieldClient = /* PolymorphicUploadFieldClient | */ SingleUploadFieldClient
export type CodeField = {
admin?: {

View File

@@ -138,7 +138,7 @@ export const promise = async <T>({
siblingData[field.name] === 'null' ||
siblingData[field.name] === null
) {
if (field.type === 'relationship' && field.hasMany === true) {
if (field.hasMany === true) {
siblingData[field.name] = []
} else {
siblingData[field.name] = null
@@ -153,32 +153,32 @@ export const promise = async <T>({
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === relatedDoc.relationTo,
)
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
fieldAffectsData(collectionField) && collectionField.name === 'id',
)
if (relationshipIDField?.type === 'number') {
siblingData[field.name][i] = {
...relatedDoc,
value: parseFloat(relatedDoc.value as string),
}
}
}
})
}
if (field.hasMany !== true && valueIsValueWithRelation(value)) {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === value.relationTo,
)
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
fieldAffectsData(collectionField) && collectionField.name === 'id',
)
if (relationshipIDField?.type === 'number') {
siblingData[field.name][i] = {
...relatedDoc,
value: parseFloat(relatedDoc.value as string),
}
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) }
}
})
}
if (
field.type === 'relationship' &&
field.hasMany !== true &&
valueIsValueWithRelation(value)
) {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === value.relationTo,
)
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
fieldAffectsData(collectionField) && collectionField.name === 'id',
)
if (relationshipIDField?.type === 'number') {
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) }
}
}
} else {
@@ -187,25 +187,31 @@ export const promise = async <T>({
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === field.relationTo,
)
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
fieldAffectsData(collectionField) && collectionField.name === 'id',
)
if (relationshipIDField?.type === 'number') {
siblingData[field.name][i] = parseFloat(relatedDoc as string)
}
}
})
}
if (field.hasMany !== true && value) {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === field.relationTo,
)
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
fieldAffectsData(collectionField) && collectionField.name === 'id',
)
if (relationshipIDField?.type === 'number') {
siblingData[field.name][i] = parseFloat(relatedDoc as string)
siblingData[field.name] = parseFloat(value as string)
}
})
}
if (field.type === 'relationship' && field.hasMany !== true && value) {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === field.relationTo,
)
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
fieldAffectsData(collectionField) && collectionField.name === 'id',
)
if (relationshipIDField?.type === 'number') {
siblingData[field.name] = parseFloat(value as string)
}
}
}

View File

@@ -571,18 +571,76 @@ const validateFilterOptions: Validate<
}
export type UploadFieldValidation = Validate<unknown, unknown, unknown, UploadField>
export const upload: UploadFieldValidation = (value: string, options) => {
if (!value && options.required) {
return options?.req?.t('validation:required')
export const upload: UploadFieldValidation = async (value, options) => {
const {
maxRows,
minRows,
relationTo,
req: { payload, t },
required,
} = options
if (
((!value && typeof value !== 'number') || (Array.isArray(value) && value.length === 0)) &&
required
) {
return t('validation:required')
}
if (Array.isArray(value) && value.length > 0) {
if (minRows && value.length < minRows) {
return t('validation:lessThanMin', {
label: t('general:rows'),
min: minRows,
value: value.length,
})
}
if (maxRows && value.length > maxRows) {
return t('validation:greaterThanMax', {
label: t('general:rows'),
max: maxRows,
value: value.length,
})
}
}
if (typeof value !== 'undefined' && value !== null) {
const idType =
options?.req?.payload?.collections[options.relationTo]?.customIDType ||
options?.req?.payload?.db?.defaultIDType
const values = Array.isArray(value) ? value : [value]
if (!isValidID(value, idType)) {
return options.req?.t('validation:validUploadID')
const invalidRelationships = values.filter((val) => {
let collectionSlug: string
let requestedID
if (typeof relationTo === 'string') {
collectionSlug = relationTo
// custom id
if (val || typeof val === 'number') {
requestedID = val
}
}
if (Array.isArray(relationTo) && typeof val === 'object' && val?.relationTo) {
collectionSlug = val.relationTo
requestedID = val.value
}
if (requestedID === null) return false
const idType =
payload.collections[collectionSlug]?.customIDType || payload?.db?.defaultIDType || 'text'
return !isValidID(requestedID, idType)
})
if (invalidRelationships.length > 0) {
return `This relationship field has the following invalid relationships: ${invalidRelationships
.map((err, invalid) => {
return `${err} ${JSON.stringify(invalid)}`
})
.join(', ')}`
}
}

View File

@@ -291,6 +291,7 @@ export function fieldsToJSONSchema(
break
}
case 'upload':
case 'relationship': {
if (Array.isArray(field.relationTo)) {
if (field.hasMany) {
@@ -380,21 +381,6 @@ export function fieldsToJSONSchema(
break
}
case 'upload': {
fieldSchema = {
oneOf: [
{
type: collectionIDFieldTypes[field.relationTo],
},
{
$ref: `#/definitions/${field.relationTo}`,
},
],
}
if (!isRequired) fieldSchema.oneOf.push({ type: 'null' })
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

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '../../scss/styles.scss';
.relationship-add-new {
display: flex;
@@ -10,8 +10,12 @@
height: 100%;
}
&__add-button,
&__add-button.doc-drawer__toggler {
&__add-button {
position: relative;
}
&__add-button--unstyled,
&__add-button--unstyled.doc-drawer__toggler {
@include formInput;
margin: 0;
border-top-left-radius: 0;

View File

@@ -4,29 +4,30 @@ import type { ClientCollectionConfig } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React, { Fragment, useCallback, useEffect, useState } from 'react'
import type { DocumentInfoContext } from '../../../providers/DocumentInfo/types.js'
import type { Value } from '../types.js'
import type { Value } from '../../fields/Relationship/types.js'
import type { DocumentInfoContext } from '../../providers/DocumentInfo/types.js'
import type { Props } from './types.js'
import { Button } from '../../../elements/Button/index.js'
import { useDocumentDrawer } from '../../../elements/DocumentDrawer/index.js'
import * as PopupList from '../../../elements/Popup/PopupButtonList/index.js'
import { Popup } from '../../../elements/Popup/index.js'
import { Tooltip } from '../../../elements/Tooltip/index.js'
import { PlusIcon } from '../../../icons/Plus/index.js'
import { useAuth } from '../../../providers/Auth/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { PlusIcon } from '../../icons/Plus/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { Button } from '../Button/index.js'
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
import * as PopupList from '../Popup/PopupButtonList/index.js'
import { Popup } from '../Popup/index.js'
import { Tooltip } from '../Tooltip/index.js'
import './index.scss'
import { useRelatedCollections } from './useRelatedCollections.js'
const baseClass = 'relationship-add-new'
export const AddNewRelation: React.FC<Props> = ({
// dispatchOptions,
Button: ButtonFromProps,
hasMany,
path,
relationTo,
setValue,
unstyled,
value,
}) => {
const relatedCollections = useRelatedCollections(relationTo)
@@ -129,23 +130,34 @@ export const AddNewRelation: React.FC<Props> = ({
}
}, [isDrawerOpen, relatedToMany])
const label = t('fields:addNewLabel', {
label: getTranslation(relatedCollections[0].labels.singular, i18n),
})
if (show) {
return (
<div className={baseClass} id={`${path}-add-new`}>
{relatedCollections.length === 1 && (
<Fragment>
<DocumentDrawerToggler
className={`${baseClass}__add-button`}
className={[
`${baseClass}__add-button`,
!unstyled && `${baseClass}__add-button--styled`,
].join(' ')}
onClick={() => setShowTooltip(false)}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<Tooltip className={`${baseClass}__tooltip`} show={showTooltip}>
{t('fields:addNewLabel', {
label: getTranslation(relatedCollections[0].labels.singular, i18n),
})}
</Tooltip>
<PlusIcon />
{ButtonFromProps ? (
ButtonFromProps
) : (
<Fragment>
<Tooltip className={`${baseClass}__tooltip`} show={showTooltip}>
{label}
</Tooltip>
<PlusIcon />
</Fragment>
)}
</DocumentDrawerToggler>
<DocumentDrawer onSave={onSave} />
</Fragment>
@@ -154,13 +166,17 @@ export const AddNewRelation: React.FC<Props> = ({
<Fragment>
<Popup
button={
<Button
buttonStyle="none"
className={`${baseClass}__add-button`}
tooltip={popupOpen ? undefined : t('fields:addNew')}
>
<PlusIcon />
</Button>
ButtonFromProps ? (
ButtonFromProps
) : (
<Button
buttonStyle="none"
className={`${baseClass}__add-button`}
tooltip={popupOpen ? undefined : t('fields:addNew')}
>
<PlusIcon />
</Button>
)
}
buttonType="custom"
horizontalAlign="center"

View File

@@ -0,0 +1,11 @@
import type { Value } from '../../fields/Relationship/types.js'
export type Props = {
readonly Button?: React.ReactNode
readonly hasMany: boolean
readonly path: string
readonly relationTo: string | string[]
readonly setValue: (value: unknown) => void
readonly unstyled?: boolean
readonly value: Value | Value[]
}

View File

@@ -3,7 +3,7 @@ import type { ClientCollectionConfig } from 'payload'
import { useState } from 'react'
import { useConfig } from '../../../providers/Config/index.js'
import { useConfig } from '../../providers/Config/index.js'
export const useRelatedCollections = (relationTo: string | string[]): ClientCollectionConfig[] => {
const { config } = useConfig()

View File

@@ -6,13 +6,13 @@ import { toast } from 'sonner'
import type { DocumentDrawerProps } from './types.js'
import { useRelatedCollections } from '../../fields/Relationship/AddNew/useRelatedCollections.js'
import { XIcon } from '../../icons/X/index.js'
import { RenderComponent } from '../../providers/Config/RenderComponent.js'
import { useConfig } from '../../providers/Config/index.js'
import { DocumentInfoProvider, useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useLocale } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { useRelatedCollections } from '../AddNewRelation/useRelatedCollections.js'
import { Gutter } from '../Gutter/index.js'
import { IDLabel } from '../IDLabel/index.js'
import { RenderTitle } from '../RenderTitle/index.js'

View File

@@ -5,9 +5,9 @@ import React, { useCallback, useEffect, useId, useMemo, useState } from 'react'
import type { DocumentDrawerProps, DocumentTogglerProps, UseDocumentDrawer } from './types.js'
import { useRelatedCollections } from '../../fields/Relationship/AddNew/useRelatedCollections.js'
import { useEditDepth } from '../../providers/EditDepth/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { useRelatedCollections } from '../AddNewRelation/useRelatedCollections.js'
import { Drawer, DrawerToggler } from '../Drawer/index.js'
import { DocumentDrawerContent } from './DrawerContent.js'
import './index.scss'

View File

@@ -0,0 +1,33 @@
@import '../../../scss/styles.scss';
.file-details-draggable {
display: flex;
gap: 0.6rem;
//justify-content: space-between;
align-items: center;
background: var(--theme-elevation-50);
border-radius: 3px;
padding: 0.7rem 0.8rem;
&--drag-wrapper {
display: flex;
gap: 0.6rem;
align-items: center;
}
&__thumbnail {
max-width: 1.5rem;
}
&__actions {
flex-grow: 2;
display: flex;
gap: 0.6rem;
align-items: center;
justify-content: flex-end;
}
&__remove.btn--style-icon-label {
margin: 0;
}
}

View File

@@ -0,0 +1,115 @@
'use client'
import React from 'react'
import { Button } from '../../Button/index.js'
import { Thumbnail } from '../../Thumbnail/index.js'
import { UploadActions } from '../../Upload/index.js'
import { FileMeta } from '../FileMeta/index.js'
import './index.scss'
const baseClass = 'file-details-draggable'
import type { Data, FileSizes, SanitizedCollectionConfig } from 'payload'
import { DraggableSortableItem } from '../../../elements/DraggableSortable/DraggableSortableItem/index.js'
import { DragHandleIcon } from '../../../icons/DragHandle/index.js'
import { EditIcon } from '../../../icons/Edit/index.js'
import { useDocumentDrawer } from '../../DocumentDrawer/index.js'
export type DraggableFileDetailsProps = {
collectionSlug: string
customUploadActions?: React.ReactNode[]
doc: {
sizes?: FileSizes
} & Data
enableAdjustments?: boolean
hasImageSizes?: boolean
hasMany: boolean
imageCacheTag?: string
isSortable?: boolean
removeItem?: (index: number) => void
rowIndex: number
uploadConfig: SanitizedCollectionConfig['upload']
}
export const DraggableFileDetails: React.FC<DraggableFileDetailsProps> = (props) => {
const {
collectionSlug,
customUploadActions,
doc,
enableAdjustments,
hasImageSizes,
hasMany,
imageCacheTag,
isSortable,
removeItem,
rowIndex,
uploadConfig,
} = props
const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc
const [DocumentDrawer, DocumentDrawerToggler] = useDocumentDrawer({
id,
collectionSlug,
})
return (
<DraggableSortableItem id={id} key={id}>
{(draggableSortableItemProps) => (
<div
className={[
baseClass,
draggableSortableItemProps && isSortable && `${baseClass}--has-drag-handle`,
]
.filter(Boolean)
.join(' ')}
ref={draggableSortableItemProps.setNodeRef}
style={{
transform: draggableSortableItemProps.transform,
transition: draggableSortableItemProps.transition,
zIndex: draggableSortableItemProps.isDragging ? 1 : undefined,
}}
>
<div className={`${baseClass}--drag-wrapper`}>
{isSortable && draggableSortableItemProps && (
<div
className={`${baseClass}__drag`}
{...draggableSortableItemProps.attributes}
{...draggableSortableItemProps.listeners}
>
<DragHandleIcon />
</div>
)}
<Thumbnail
className={`${baseClass}__thumbnail`}
collectionSlug={collectionSlug}
doc={doc}
fileSrc={thumbnailURL || url}
imageCacheTag={imageCacheTag}
uploadConfig={uploadConfig}
/>
</div>
<div className={`${baseClass}__main-detail`}>{filename}</div>
<div className={`${baseClass}__actions`}>
<DocumentDrawer />
<DocumentDrawerToggler>
<EditIcon />
</DocumentDrawerToggler>
{removeItem && (
<Button
buttonStyle="icon-label"
className={`${baseClass}__remove`}
icon="x"
iconStyle="none"
onClick={() => removeItem(rowIndex)}
round
/>
)}
</div>
</div>
)}
</DraggableSortableItem>
)
}

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss';
@import '../../../scss/styles.scss';
.file-details {
background: var(--theme-elevation-50);

View File

@@ -0,0 +1,87 @@
'use client'
import React from 'react'
import { Button } from '../../Button/index.js'
import { Thumbnail } from '../../Thumbnail/index.js'
import { UploadActions } from '../../Upload/index.js'
import { FileMeta } from '../FileMeta/index.js'
import './index.scss'
const baseClass = 'file-details'
import type { Data, FileSizes, SanitizedCollectionConfig } from 'payload'
export type StaticFileDetailsProps = {
collectionSlug: string
customUploadActions?: React.ReactNode[]
doc: {
sizes?: FileSizes
} & Data
enableAdjustments?: boolean
handleRemove?: () => void
hasImageSizes?: boolean
imageCacheTag?: string
uploadConfig: SanitizedCollectionConfig['upload']
}
export const StaticFileDetails: React.FC<StaticFileDetailsProps> = (props) => {
const {
collectionSlug,
customUploadActions,
doc,
enableAdjustments,
handleRemove,
hasImageSizes,
imageCacheTag,
uploadConfig,
} = props
const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc
return (
<div className={baseClass}>
<header>
<Thumbnail
// size="small"
className={`${baseClass}__thumbnail`}
collectionSlug={collectionSlug}
doc={doc}
fileSrc={thumbnailURL || url}
imageCacheTag={imageCacheTag}
uploadConfig={uploadConfig}
/>
<div className={`${baseClass}__main-detail`}>
<FileMeta
collection={collectionSlug}
filename={filename as string}
filesize={filesize as number}
height={height as number}
id={id as string}
mimeType={mimeType as string}
url={url as string}
width={width as number}
/>
{(enableAdjustments || customUploadActions) && (
<UploadActions
customActions={customUploadActions}
enableAdjustments={Boolean(enableAdjustments)}
enablePreviewSizes={hasImageSizes && doc.filename}
mimeType={mimeType}
/>
)}
</div>
{handleRemove && (
<Button
buttonStyle="icon-label"
className={`${baseClass}__remove`}
icon="x"
iconStyle="with-border"
onClick={handleRemove}
round
/>
)}
</header>
</div>
)
}

View File

@@ -1,87 +1,49 @@
'use client'
import React from 'react'
import { UploadActions } from '../../elements/Upload/index.js'
import { Button } from '../Button/index.js'
import { Thumbnail } from '../Thumbnail/index.js'
import { FileMeta } from './FileMeta/index.js'
import './index.scss'
const baseClass = 'file-details'
import type { Data, FileSizes, SanitizedCollectionConfig } from 'payload'
export type FileDetailsProps = {
import React from 'react'
import { DraggableFileDetails } from './DraggableFileDetails/index.js'
import { StaticFileDetails } from './StaticFileDetails/index.js'
type SharedFileDetailsProps = {
collectionSlug: string
customUploadActions?: React.ReactNode[]
doc: {
sizes?: FileSizes
} & Data
enableAdjustments?: boolean
handleRemove?: () => void
hasImageSizes?: boolean
imageCacheTag?: string
uploadConfig: SanitizedCollectionConfig['upload']
}
export const FileDetails: React.FC<FileDetailsProps> = (props) => {
const {
collectionSlug,
customUploadActions,
doc,
enableAdjustments,
handleRemove,
hasImageSizes,
imageCacheTag,
uploadConfig,
} = props
const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc
return (
<div className={baseClass}>
<header>
<Thumbnail
// size="small"
className={`${baseClass}__thumbnail`}
collectionSlug={collectionSlug}
doc={doc}
fileSrc={thumbnailURL || url}
imageCacheTag={imageCacheTag}
uploadConfig={uploadConfig}
/>
<div className={`${baseClass}__main-detail`}>
<FileMeta
collection={collectionSlug}
filename={filename as string}
filesize={filesize as number}
height={height as number}
id={id as string}
mimeType={mimeType as string}
url={url as string}
width={width as number}
/>
{(enableAdjustments || customUploadActions) && (
<UploadActions
customActions={customUploadActions}
enableAdjustments={enableAdjustments}
enablePreviewSizes={hasImageSizes && doc.filename}
mimeType={mimeType}
/>
)}
</div>
{handleRemove && (
<Button
buttonStyle="icon-label"
className={`${baseClass}__remove`}
icon="x"
iconStyle="with-border"
onClick={handleRemove}
round
/>
)}
</header>
</div>
)
type StaticFileDetailsProps = {
draggableItemProps?: never
handleRemove?: () => void
hasMany?: never
isSortable?: never
removeItem?: never
rowIndex?: never
}
type DraggableFileDetailsProps = {
handleRemove?: never
hasMany: boolean
isSortable?: boolean
removeItem?: (index: number) => void
rowIndex: number
}
export type FileDetailsProps = (DraggableFileDetailsProps | StaticFileDetailsProps) &
SharedFileDetailsProps
export const FileDetails: React.FC<FileDetailsProps> = (props) => {
const { hasMany } = props
if (hasMany) {
return <DraggableFileDetails {...props} />
}
return <StaticFileDetails {...props} />
}

View File

@@ -3,7 +3,7 @@ import type { ClientCollectionConfig, ClientField, Where } from 'payload'
import { useWindowInfo } from '@faceless-ui/window-info'
import { getTranslation } from '@payloadcms/translations'
import React, { useEffect, useRef, useState } from 'react'
import React, { Fragment, useEffect, useRef, useState } from 'react'
import AnimateHeightImport from 'react-animate-height'
const AnimateHeight = (AnimateHeightImport.default ||
@@ -49,7 +49,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
const { collectionConfig, enableColumns = true, enableSort = false, fields } = props
const { handleSearchChange } = useListQuery()
const { collectionSlug } = useListInfo()
const { beforeActions, collectionSlug, disableBulkDelete, disableBulkEdit } = useListInfo()
const { searchParams } = useSearchParams()
const titleField = useUseTitleField(collectionConfig, fields)
const { i18n, t } = useTranslation()
@@ -144,10 +144,15 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
<div className={`${baseClass}__buttons-wrap`}>
{!smallBreak && (
<React.Fragment>
<EditMany collection={collectionConfig} fields={fields} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
<DeleteMany collection={collectionConfig} />
{beforeActions && beforeActions}
{!disableBulkEdit && (
<Fragment>
<EditMany collection={collectionConfig} fields={fields} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
</Fragment>
)}
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
</React.Fragment>
)}
{enableColumns && (

View File

@@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useReducer, useState } from 'react'
import type { ListDrawerProps } from './types.js'
import { SelectMany } from '../../elements/SelectMany/index.js'
import { FieldLabel } from '../../fields/FieldLabel/index.js'
import { usePayloadAPI } from '../../hooks/usePayloadAPI.js'
import { XIcon } from '../../icons/X/index.js'
@@ -45,7 +46,9 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
collectionSlugs,
customHeader,
drawerSlug,
enableRowSelections,
filterOptions,
onBulkSelect,
onSelect,
selectedCollection,
}) => {
@@ -276,8 +279,13 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
)}
</header>
}
beforeActions={
enableRowSelections ? [<SelectMany key="select-many" onClick={onBulkSelect} />] : undefined
}
collectionConfig={selectedCollectionConfig}
collectionSlug={selectedCollectionConfig.slug}
disableBulkDelete
disableBulkEdit
hasCreatePermission={hasCreatePermission}
newDocumentURL={null}
>
@@ -309,6 +317,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
},
]}
collectionSlug={selectedCollectionConfig.slug}
enableRowSelections={enableRowSelections}
preferenceKey={preferenceKey}
>
<RenderComponent mappedComponent={List} />

View File

@@ -87,6 +87,7 @@ export const useListDrawer: UseListDrawer = ({
setCollectionSlugs(filteredCollectionSlugs.map(({ slug }) => slug))
}
}, [collectionSlugs, uploads, collections])
const toggleDrawer = useCallback(() => {
toggleModal(drawerSlug)
}, [toggleModal, drawerSlug])

View File

@@ -2,11 +2,15 @@ import type { FilterOptionsResult, SanitizedCollectionConfig } from 'payload'
import type React from 'react'
import type { HTMLAttributes } from 'react'
import type { useSelection } from '../../providers/Selection/index.js'
export type ListDrawerProps = {
readonly collectionSlugs: string[]
readonly customHeader?: React.ReactNode
readonly drawerSlug?: string
readonly enableRowSelections?: boolean
readonly filterOptions?: FilterOptionsResult
readonly onBulkSelect?: (selected: ReturnType<typeof useSelection>['selected']) => void
readonly onSelect?: (args: {
collectionSlug: SanitizedCollectionConfig['slug']
docID: string
@@ -27,7 +31,7 @@ export type UseListDrawer = (args: {
selectedCollection?: string
uploads?: boolean // finds all collections with upload: true
}) => [
React.FC<Pick<ListDrawerProps, 'onSelect'>>, // drawer
React.FC<Pick<ListDrawerProps, 'enableRowSelections' | 'onBulkSelect' | 'onSelect'>>, // drawer
React.FC<Pick<ListTogglerProps, 'children' | 'className' | 'disabled'>>, // toggler
{
closeDrawer: () => void

View File

@@ -0,0 +1,32 @@
import React from 'react'
import { useSelection } from '../../providers/Selection/index.js'
// import { useTranslation } from '../../providers/Translation/index.js'
import { Pill } from '../Pill/index.js'
export const SelectMany: React.FC<{
onClick?: (ids: ReturnType<typeof useSelection>['selected']) => void
}> = (props) => {
const { onClick } = props
const { count, selected } = useSelection()
// const { t } = useTranslation()
if (!selected || !count) {
return null
}
return (
<Pill
// className={`${baseClass}__toggle`}
onClick={() => {
if (typeof onClick === 'function') {
onClick(selected)
}
}}
pillStyle="white"
>
{`Select ${count}`}
</Pill>
)
}

View File

@@ -1,13 +0,0 @@
import type React from 'react'
import type { Action, OptionGroup, Value } from '../types.js'
export type Props = {
dispatchOptions: React.Dispatch<Action>
hasMany: boolean
options: OptionGroup[]
path: string
relationTo: string | string[]
setValue: (value: unknown) => void
value: Value | Value[]
}

View File

@@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useReducer, useRef, useState } from 'rea
import type { DocumentDrawerProps } from '../../elements/DocumentDrawer/types.js'
import type { GetResults, Option, Value } from './types.js'
import { AddNewRelation } from '../../elements/AddNewRelation/index.js'
import { ReactSelect } from '../../elements/ReactSelect/index.js'
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
import { useField } from '../../forms/useField/index.js'
@@ -21,7 +22,6 @@ import { FieldDescription } from '../FieldDescription/index.js'
import { FieldError } from '../FieldError/index.js'
import { FieldLabel } from '../FieldLabel/index.js'
import { fieldBaseClass } from '../shared/index.js'
import { AddNewRelation } from './AddNew/index.js'
import { createRelationMap } from './createRelationMap.js'
import { findOptionsByValue } from './findOptionsByValue.js'
import './index.scss'
@@ -591,15 +591,11 @@ const RelationshipFieldComponent: React.FC<RelationshipFieldProps> = (props) =>
/>
{!readOnly && allowCreate && (
<AddNewRelation
{...{
dispatchOptions,
hasMany,
options,
path,
relationTo,
setValue,
value,
}}
hasMany={hasMany}
path={path}
relationTo={relationTo}
setValue={setValue}
value={value}
/>
)}
</div>

View File

@@ -0,0 +1,47 @@
@import '../../../scss/styles.scss';
.upload--has-many {
position: relative;
max-width: 100%;
&__controls {
display: flex;
gap: base(2);
margin-top: base(1);
}
&__buttons {
display: flex;
gap: base(1);
}
&__add-new {
display: flex;
gap: base(0.5);
}
&__clear-all {
all: unset;
flex-shrink: 0;
font-size: inherit;
font-family: inherit;
color: inherit;
cursor: pointer;
}
&__draggable-rows {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
}
html[data-theme='light'] {
.upload {
}
}
html[data-theme='dark'] {
.upload {
}
}

View File

@@ -0,0 +1,256 @@
'use client'
import type { FilterOptionsResult, Where } from 'payload'
import * as qs from 'qs-esm'
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
import type { useSelection } from '../../../providers/Selection/index.js'
import type { UploadFieldPropsWithContext } from '../HasOne/index.js'
import { AddNewRelation } from '../../../elements/AddNewRelation/index.js'
import { Button } from '../../../elements/Button/index.js'
import { DraggableSortable } from '../../../elements/DraggableSortable/index.js'
import { FileDetails } from '../../../elements/FileDetails/index.js'
import { useListDrawer } from '../../../elements/ListDrawer/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useLocale } from '../../../providers/Locale/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { FieldLabel } from '../../FieldLabel/index.js'
const baseClass = 'upload upload--has-many'
import './index.scss'
export const UploadComponentHasMany: React.FC<UploadFieldPropsWithContext<string[]>> = (props) => {
const {
canCreate,
field,
field: {
_path,
admin: {
components: { Label },
isSortable,
},
hasMany,
label,
relationTo,
},
fieldHookResult: { filterOptions: filterOptionsFromProps, setValue, value },
readOnly,
} = props
const { i18n, t } = useTranslation()
const {
config: {
collections,
routes: { api },
serverURL,
},
} = useConfig()
const filterOptions: FilterOptionsResult = useMemo(() => {
if (typeof relationTo === 'string') {
return {
...filterOptionsFromProps,
[relationTo]: {
...((filterOptionsFromProps?.[relationTo] as any) || {}),
id: {
...((filterOptionsFromProps?.[relationTo] as any)?.id || {}),
not_in: (filterOptionsFromProps?.[relationTo] as any)?.id?.not_in || value,
},
},
}
}
}, [value, relationTo, filterOptionsFromProps])
const [fileDocs, setFileDocs] = useState([])
const [missingFiles, setMissingFiles] = useState(false)
const { code } = useLocale()
useEffect(() => {
if (value !== null && typeof value !== 'undefined' && value.length !== 0) {
const query: {
[key: string]: unknown
where: Where
} = {
depth: 0,
draft: true,
locale: code,
where: {
and: [
{
id: {
in: value,
},
},
],
},
}
const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}`, {
body: qs.stringify(query),
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/x-www-form-urlencoded',
'X-HTTP-Method-Override': 'GET',
},
method: 'POST',
})
if (response.ok) {
const json = await response.json()
setFileDocs(json.docs)
} else {
setMissingFiles(true)
setFileDocs([])
}
}
void fetchFile()
} else {
setFileDocs([])
}
}, [value, relationTo, api, serverURL, i18n, code])
function moveItemInArray<T>(array: T[], moveFromIndex: number, moveToIndex: number): T[] {
const newArray = [...array]
const [item] = newArray.splice(moveFromIndex, 1)
newArray.splice(moveToIndex, 0, item)
return newArray
}
const moveRow = useCallback(
(moveFromIndex: number, moveToIndex: number) => {
const updatedArray = moveItemInArray(value, moveFromIndex, moveToIndex)
setValue(updatedArray)
},
[value, setValue],
)
const removeItem = useCallback(
(index: number) => {
const updatedArray = [...value]
updatedArray.splice(index, 1)
setValue(updatedArray)
},
[value, setValue],
)
const [ListDrawer, ListDrawerToggler] = useListDrawer({
collectionSlugs:
typeof relationTo === 'string'
? [relationTo]
: collections.map((collection) => collection.slug),
filterOptions,
})
const collection = collections.find((coll) => coll.slug === relationTo)
const onBulkSelect = useCallback(
(selections: ReturnType<typeof useSelection>['selected']) => {
const selectedIDs = Object.entries(selections).reduce(
(acc, [key, value]) => (value ? [...acc, key] : acc),
[] as string[],
)
setValue([...value, ...selectedIDs])
},
[setValue, value],
)
return (
<Fragment>
<div className={[baseClass].join(' ')}>
<FieldLabel Label={Label} field={field} label={label} />
<div className={[baseClass].join(' ')}>
<div>
<DraggableSortable
className={`${baseClass}__draggable-rows`}
ids={value}
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
>
{Boolean(value.length) &&
value.map((id, index) => {
const doc = fileDocs.find((doc) => doc.id === id)
const uploadConfig = collection?.upload
if (!doc) {
return null
}
return (
<FileDetails
collectionSlug={relationTo}
doc={doc}
hasMany={true}
isSortable={isSortable}
key={id}
removeItem={removeItem}
rowIndex={index}
uploadConfig={uploadConfig}
/>
)
})}
</DraggableSortable>
</div>
</div>
<div className={[`${baseClass}__controls`].join(' ')}>
<div className={[`${baseClass}__buttons`].join(' ')}>
{canCreate && (
<div className={[`${baseClass}__add-new`].join(' ')}>
<AddNewRelation
Button={
<Button
buttonStyle="icon-label"
el="span"
icon="plus"
iconPosition="left"
iconStyle="with-border"
>
{t('fields:addNew')}
</Button>
}
hasMany={hasMany}
path={_path}
relationTo={relationTo}
setValue={setValue}
unstyled
value={value}
/>
</div>
)}
<ListDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<div>
<Button
buttonStyle="icon-label"
el="span"
icon="plus"
iconPosition="left"
iconStyle="with-border"
>
{t('fields:chooseFromExisting')}
</Button>
</div>
</ListDrawerToggler>
</div>
<button className={`${baseClass}__clear-all`} onClick={() => setValue([])} type="button">
Clear all
</button>
</div>
</div>
<ListDrawer
enableRowSelections
onBulkSelect={onBulkSelect}
onSelect={(selection) => {
setValue([...value, selection.docID])
}}
/>
</Fragment>
)
}

View File

@@ -17,22 +17,21 @@ import type { MarkOptional } from 'ts-essentials'
import { getTranslation } from '@payloadcms/translations'
import React, { useCallback, useEffect, useState } from 'react'
import type { DocumentDrawerProps } from '../../elements/DocumentDrawer/types.js'
import type { ListDrawerProps } from '../../elements/ListDrawer/types.js'
import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types.js'
import type { ListDrawerProps } from '../../../elements/ListDrawer/types.js'
import { Button } from '../../elements/Button/index.js'
import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js'
import { FileDetails } from '../../elements/FileDetails/index.js'
import { useListDrawer } from '../../elements/ListDrawer/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { FieldDescription } from '../FieldDescription/index.js'
import { FieldError } from '../FieldError/index.js'
import { FieldLabel } from '../FieldLabel/index.js'
import { fieldBaseClass } from '../shared/index.js'
import { Button } from '../../../elements/Button/index.js'
import { useDocumentDrawer } from '../../../elements/DocumentDrawer/index.js'
import { FileDetails } from '../../../elements/FileDetails/index.js'
import { useListDrawer } from '../../../elements/ListDrawer/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { FieldDescription } from '../../FieldDescription/index.js'
import { FieldError } from '../../FieldError/index.js'
import { FieldLabel } from '../../FieldLabel/index.js'
import { fieldBaseClass } from '../../shared/index.js'
import { baseClass } from '../index.js'
import './index.scss'
const baseClass = 'upload'
export type UploadInputProps = {
readonly Description?: MappedComponent
readonly Error?: MappedComponent
@@ -63,7 +62,7 @@ export type UploadInputProps = {
readonly width?: string
}
export const UploadInput: React.FC<UploadInputProps> = (props) => {
export const UploadInputHasOne: React.FC<UploadInputProps> = (props) => {
const {
Description,
Error,
@@ -149,7 +148,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
[onChange, closeListDrawer],
)
if (collection.upload) {
if (collection.upload && typeof relationTo === 'string') {
return (
<div
className={[

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss';
@import '../../../scss/styles.scss';
.upload {
position: relative;

View File

@@ -0,0 +1,76 @@
'use client'
import type { UploadFieldProps } from 'payload'
import React from 'react'
import type { useField } from '../../../forms/useField/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { UploadInputHasOne } from './Input.js'
import './index.scss'
export type UploadFieldPropsWithContext<TValue extends string | string[] = string> = {
readonly canCreate: boolean
readonly disabled: boolean
readonly fieldHookResult: ReturnType<typeof useField<TValue>>
readonly onChange: (value: unknown) => void
} & UploadFieldProps
export const UploadComponentHasOne: React.FC<UploadFieldPropsWithContext> = (props) => {
const {
canCreate,
descriptionProps,
disabled,
errorProps,
field,
field: { admin: { className, style, width } = {}, label, relationTo, required },
fieldHookResult,
labelProps,
onChange,
} = props
const {
config: {
collections,
routes: { api: apiRoute },
serverURL,
},
} = useConfig()
if (typeof relationTo === 'string') {
const collection = collections.find((coll) => coll.slug === relationTo)
if (collection.upload) {
return (
<UploadInputHasOne
Description={field?.admin?.components?.Description}
Error={field?.admin?.components?.Error}
Label={field?.admin?.components?.Label}
allowNewUpload={canCreate}
api={apiRoute}
className={className}
collection={collection}
descriptionProps={descriptionProps}
errorProps={errorProps}
filterOptions={fieldHookResult.filterOptions}
label={label}
labelProps={labelProps}
onChange={onChange}
readOnly={disabled}
relationTo={relationTo}
required={required}
serverURL={serverURL}
showError={fieldHookResult.showError}
style={style}
value={fieldHookResult.value}
width={width}
/>
)
}
} else {
return <div>Polymorphic Has One Uploads Go Here</div>
}
return null
}

View File

@@ -4,50 +4,38 @@ import type { UploadFieldProps } from 'payload'
import React, { useCallback, useMemo } from 'react'
import type { UploadInputProps } from './Input.js'
import type { UploadInputProps } from './HasOne/Input.js'
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
import { useField } from '../../forms/useField/index.js'
import { withCondition } from '../../forms/withCondition/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { UploadInput } from './Input.js'
import './index.scss'
import { UploadComponentHasMany } from './HasMany/index.js'
import { UploadInputHasOne } from './HasOne/Input.js'
import { UploadComponentHasOne } from './HasOne/index.js'
export { UploadFieldProps, UploadInput }
export { UploadFieldProps, UploadInputHasOne as UploadInput }
export type { UploadInputProps }
export const baseClass = 'upload'
const UploadComponent: React.FC<UploadFieldProps> = (props) => {
const {
descriptionProps,
errorProps,
field,
field: {
_path: pathFromProps,
admin: { className, readOnly: readOnlyFromAdmin, style, width } = {},
label,
admin: { readOnly: readOnlyFromAdmin } = {},
hasMany,
relationTo,
required,
},
labelProps,
readOnly: readOnlyFromTopLevelProps,
validate,
} = props
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
const {
config: {
collections,
routes: { api: apiRoute },
serverURL,
},
} = useConfig()
const { permissions } = useAuth()
const collection = collections.find((coll) => coll.slug === relationTo)
const memoizedValidate = useCallback(
(value, options) => {
if (typeof validate === 'function') {
@@ -61,22 +49,29 @@ const UploadComponent: React.FC<UploadFieldProps> = (props) => {
// Checks if the user has permissions to create a new document in the related collection
const canCreate = useMemo(() => {
if (permissions?.collections && permissions.collections?.[relationTo]?.create) {
if (permissions.collections[relationTo].create?.permission === true) {
return true
if (typeof relationTo === 'string') {
if (permissions?.collections && permissions.collections?.[relationTo]?.create) {
if (permissions.collections[relationTo].create?.permission === true) {
return true
}
}
}
return false
}, [relationTo, permissions])
const { filterOptions, formInitializing, formProcessing, setValue, showError, value } =
useField<string>({
path: pathFromContext ?? pathFromProps,
validate: memoizedValidate,
})
const fieldHookResult = useField<string | string[]>({
path: pathFromContext ?? pathFromProps,
validate: memoizedValidate,
})
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
const setValue = useMemo(() => fieldHookResult.setValue, [fieldHookResult])
const disabled =
readOnlyFromProps ||
readOnlyFromContext ||
fieldHookResult.formProcessing ||
fieldHookResult.formInitializing
const onChange = useCallback(
(incomingValue) => {
@@ -86,35 +81,31 @@ const UploadComponent: React.FC<UploadFieldProps> = (props) => {
[setValue],
)
if (collection.upload) {
if (hasMany) {
return (
<UploadInput
Description={field?.admin?.components?.Description}
Error={field?.admin?.components?.Error}
Label={field?.admin?.components?.Label}
allowNewUpload={canCreate}
api={apiRoute}
className={className}
collection={collection}
descriptionProps={descriptionProps}
errorProps={errorProps}
filterOptions={filterOptions}
label={label}
labelProps={labelProps}
<UploadComponentHasMany
{...props}
canCreate={canCreate}
disabled={disabled}
// Note: the below TS error is thrown bc the field hook return result varies based on `hasMany`
// @ts-expect-error
fieldHookResult={fieldHookResult}
onChange={onChange}
readOnly={disabled}
relationTo={relationTo}
required={required}
serverURL={serverURL}
showError={showError}
style={style}
value={value}
width={width}
/>
)
}
return null
return (
<UploadComponentHasOne
{...props}
canCreate={canCreate}
disabled={disabled}
// Note: the below TS error is thrown bc the field hook return result varies based on `hasMany`
// @ts-expect-error
fieldHookResult={fieldHookResult}
onChange={onChange}
/>
)
}
export const UploadField = withCondition(UploadComponent)

View File

@@ -381,7 +381,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
break
}
case 'upload':
case 'relationship': {
if (field.filterOptions) {
if (typeof field.filterOptions === 'object') {
@@ -467,41 +467,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
break
}
case 'upload': {
if (field.filterOptions) {
if (typeof field.filterOptions === 'object') {
fieldState.filterOptions = {
[field.relationTo]: field.filterOptions,
}
}
if (typeof field.filterOptions === 'function') {
const query = await getFilterOptionsQuery(field.filterOptions, {
id,
data: fullData,
relationTo: field.relationTo,
siblingData: data,
user: req.user,
})
fieldState.filterOptions = query
}
}
const relationshipValue =
data[field.name] && typeof data[field.name] === 'object' && 'id' in data[field.name]
? data[field.name].id
: data[field.name]
fieldState.value = relationshipValue
fieldState.initialValue = relationshipValue
if (!filter || filter(args)) {
state[`${path}${field.name}`] = fieldState
}
break
}
default: {
fieldState.value = data[field.name]
fieldState.initialValue = data[field.name]

View File

@@ -9,8 +9,11 @@ export type ColumnPreferences = Pick<Column, 'accessor' | 'active'>[]
export type ListInfoProps = {
readonly Header?: React.ReactNode
readonly beforeActions?: React.ReactNode[]
readonly collectionConfig: ClientConfig['collections'][0]
readonly collectionSlug: SanitizedCollectionConfig['slug']
readonly disableBulkDelete?: boolean
readonly disableBulkEdit?: boolean
readonly hasCreatePermission: boolean
readonly newDocumentURL: string
readonly titleField?: FieldAffectingData
@@ -18,7 +21,10 @@ export type ListInfoProps = {
export type ListInfoContext = {
readonly Header?: React.ReactNode
readonly beforeActions?: React.ReactNode[]
readonly collectionSlug: string
readonly disableBulkDelete?: boolean
readonly disableBulkEdit?: boolean
readonly hasCreatePermission: boolean
readonly newDocumentURL: string
} & ListInfoProps

View File

@@ -16,6 +16,8 @@ export enum SelectAllStatus {
type SelectionContext = {
count: number
disableBulkDelete?: boolean
disableBulkEdit?: boolean
getQueryParams: (additionalParams?: Where) => string
selectAll: SelectAllStatus
selected: Record<number | string, boolean>
@@ -27,10 +29,11 @@ type SelectionContext = {
const Context = createContext({} as SelectionContext)
type Props = {
children: React.ReactNode
docs: any[]
totalDocs: number
readonly children: React.ReactNode
readonly docs: any[]
readonly totalDocs: number
}
export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalDocs }) => {
const contextRef = useRef({} as SelectionContext)

View File

@@ -0,0 +1 @@
uploads

View File

@@ -0,0 +1,21 @@
import type { CollectionConfig } from 'payload'
import { uploadsMulti, uploadsSlug } from '../../slugs.js'
const Uploads: CollectionConfig = {
slug: uploadsMulti,
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'media',
type: 'upload',
hasMany: true,
relationTo: uploadsSlug,
},
],
}
export default Uploads

View File

@@ -0,0 +1,5 @@
import type { Upload } from '../../payload-types.js'
export const uploadsDoc: Partial<Upload> = {
text: 'An upload here',
}

View File

@@ -0,0 +1 @@
uploads

View File

@@ -0,0 +1,21 @@
import type { CollectionConfig } from 'payload'
import { uploads2Slug, uploadsMultiPoly, uploadsSlug } from '../../slugs.js'
const Uploads: CollectionConfig = {
slug: uploadsMultiPoly,
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'media',
type: 'upload',
hasMany: true,
relationTo: [uploadsSlug, uploads2Slug],
},
],
}
export default Uploads

View File

@@ -0,0 +1,5 @@
import type { Upload } from '../../payload-types.js'
export const uploadsDoc: Partial<Upload> = {
text: 'An upload here',
}

View File

@@ -0,0 +1 @@
uploads

View File

@@ -0,0 +1,20 @@
import type { CollectionConfig } from 'payload'
import { uploads2Slug, uploadsPoly, uploadsSlug } from '../../slugs.js'
const Uploads: CollectionConfig = {
slug: uploadsPoly,
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'media',
type: 'upload',
relationTo: [uploadsSlug, uploads2Slug],
},
],
}
export default Uploads

View File

@@ -0,0 +1,5 @@
import type { Upload } from '../../payload-types.js'
export const uploadsDoc: Partial<Upload> = {
text: 'An upload here',
}

View File

@@ -33,6 +33,9 @@ import TextFields from './collections/Text/index.js'
import UIFields from './collections/UI/index.js'
import Uploads from './collections/Upload/index.js'
import Uploads2 from './collections/Upload2/index.js'
import UploadsMulti from './collections/UploadMulti/index.js'
import UploadsMultiPoly from './collections/UploadMultiPoly/index.js'
import UploadsPoly from './collections/UploadPoly/index.js'
import Uploads3 from './collections/Uploads3/index.js'
import TabsWithRichText from './globals/TabsWithRichText.js'
import { clearAndSeedEverything } from './seed.js'
@@ -79,6 +82,9 @@ export const collectionSlugs: CollectionConfig[] = [
Uploads,
Uploads2,
Uploads3,
UploadsMulti,
UploadsPoly,
UploadsMultiPoly,
UIFields,
]

View File

@@ -56,6 +56,9 @@ export interface Config {
uploads: Upload;
uploads2: Uploads2;
uploads3: Uploads3;
'uploads-multi': UploadsMulti;
'uploads-poly': UploadsPoly;
'uploads-multi-poly': UploadsMultiPoly;
'ui-fields': UiField;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -1311,7 +1314,7 @@ export interface TabsField {
export interface Upload {
id: string;
text?: string | null;
media?: string | Upload | null;
media?: (string | null) | Upload;
richText?: {
root: {
type: string;
@@ -1346,7 +1349,7 @@ export interface Upload {
export interface Uploads2 {
id: string;
text?: string | null;
media?: string | Uploads2 | null;
media?: (string | null) | Uploads2;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -1365,7 +1368,7 @@ export interface Uploads2 {
*/
export interface Uploads3 {
id: string;
media?: string | Uploads3 | null;
media?: (string | null) | Uploads3;
richText?: {
root: {
type: string;
@@ -1393,6 +1396,58 @@ export interface Uploads3 {
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "uploads-multi".
*/
export interface UploadsMulti {
id: string;
text?: string | null;
media?: (string | Upload)[] | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "uploads-poly".
*/
export interface UploadsPoly {
id: string;
text?: string | null;
media?:
| ({
relationTo: 'uploads';
value: string | Upload;
} | null)
| ({
relationTo: 'uploads2';
value: string | Uploads2;
} | null);
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "uploads-multi-poly".
*/
export interface UploadsMultiPoly {
id: string;
text?: string | null;
media?:
| (
| {
relationTo: 'uploads';
value: string | Upload;
}
| {
relationTo: 'uploads2';
value: string | Uploads2;
}
)[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ui-fields".

View File

@@ -49,6 +49,10 @@ import {
tabsFieldsSlug,
textFieldsSlug,
uiSlug,
uploads2Slug,
uploadsMulti,
uploadsMultiPoly,
uploadsPoly,
uploadsSlug,
usersSlug,
} from './slugs.js'
@@ -123,6 +127,50 @@ export const seed = async (_payload: Payload) => {
overrideAccess: true,
})
// const createdJPGDocSlug2 = await _payload.create({
// collection: uploads2Slug,
// data: {
// ...uploadsDoc,
// },
// file: jpgFile,
// depth: 0,
// overrideAccess: true,
// })
// Create hasMany upload
await _payload.create({
collection: uploadsMulti,
data: {
media: [createdPNGDoc.id, createdJPGDoc.id],
},
})
// Create hasMany poly upload
// await _payload.create({
// collection: uploadsMultiPoly,
// data: {
// media: [
// { value: createdJPGDocSlug2.id, relationTo: uploads2Slug },
// { value: createdJPGDoc.id, relationTo: uploadsSlug },
// ],
// },
// })
// Create poly upload
await _payload.create({
collection: uploadsPoly,
data: {
media: { value: createdJPGDoc.id, relationTo: uploadsSlug },
},
})
// Create poly upload
// await _payload.create({
// collection: uploadsPoly,
// data: {
// media: { value: createdJPGDocSlug2.id, relationTo: uploads2Slug },
// },
// })
const formattedID =
_payload.db.defaultIDType === 'number' ? createdArrayDoc.id : `"${createdArrayDoc.id}"`

View File

@@ -26,6 +26,9 @@ export const textFieldsSlug = 'text-fields'
export const uploadsSlug = 'uploads'
export const uploads2Slug = 'uploads2'
export const uploads3Slug = 'uploads3'
export const uploadsMulti = 'uploads-multi'
export const uploadsMultiPoly = 'uploads-multi-poly'
export const uploadsPoly = 'uploads-poly'
export const uiSlug = 'ui-fields'
export const collectionSlugs = [

View File

@@ -47,7 +47,7 @@ export interface Config {
'payload-migrations': PayloadMigration;
};
db: {
defaultIDType: number;
defaultIDType: string;
};
globals: {};
locale: null;
@@ -78,9 +78,9 @@ export interface UserAuthOperations {
* via the `definition` "relation".
*/
export interface Relation {
id: number;
image?: number | Media | null;
versionedImage?: number | Version | null;
id: string;
image?: (string | null) | Media;
versionedImage?: (string | null) | Version;
updatedAt: string;
createdAt: string;
}
@@ -89,7 +89,7 @@ export interface Relation {
* via the `definition` "media".
*/
export interface Media {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -229,7 +229,7 @@ export interface Media {
* via the `definition` "versions".
*/
export interface Version {
id: number;
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
@@ -249,8 +249,8 @@ export interface Version {
* via the `definition` "audio".
*/
export interface Audio {
id: number;
audio?: number | Media | null;
id: string;
audio?: (string | null) | Media;
updatedAt: string;
createdAt: string;
}
@@ -259,7 +259,7 @@ export interface Audio {
* via the `definition` "gif-resize".
*/
export interface GifResize {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -295,7 +295,7 @@ export interface GifResize {
* via the `definition` "filename-compound-index".
*/
export interface FilenameCompoundIndex {
id: number;
id: string;
alt?: string | null;
updatedAt: string;
createdAt: string;
@@ -332,7 +332,7 @@ export interface FilenameCompoundIndex {
* via the `definition` "no-image-sizes".
*/
export interface NoImageSize {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -350,7 +350,7 @@ export interface NoImageSize {
* via the `definition` "object-fit".
*/
export interface ObjectFit {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -402,7 +402,7 @@ export interface ObjectFit {
* via the `definition` "with-meta-data".
*/
export interface WithMetaDatum {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -430,7 +430,7 @@ export interface WithMetaDatum {
* via the `definition` "without-meta-data".
*/
export interface WithoutMetaDatum {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -458,7 +458,7 @@ export interface WithoutMetaDatum {
* via the `definition` "with-only-jpeg-meta-data".
*/
export interface WithOnlyJpegMetaDatum {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -486,7 +486,7 @@ export interface WithOnlyJpegMetaDatum {
* via the `definition` "crop-only".
*/
export interface CropOnly {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -530,7 +530,7 @@ export interface CropOnly {
* via the `definition` "focal-only".
*/
export interface FocalOnly {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -574,7 +574,7 @@ export interface FocalOnly {
* via the `definition` "focal-no-sizes".
*/
export interface FocalNoSize {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -592,7 +592,7 @@ export interface FocalNoSize {
* via the `definition` "animated-type-media".
*/
export interface AnimatedTypeMedia {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -644,7 +644,7 @@ export interface AnimatedTypeMedia {
* via the `definition` "enlarge".
*/
export interface Enlarge {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -704,7 +704,7 @@ export interface Enlarge {
* via the `definition` "reduce".
*/
export interface Reduce {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -756,7 +756,7 @@ export interface Reduce {
* via the `definition` "media-trim".
*/
export interface MediaTrim {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -800,7 +800,7 @@ export interface MediaTrim {
* via the `definition` "custom-file-name-media".
*/
export interface CustomFileNameMedia {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -828,7 +828,7 @@ export interface CustomFileNameMedia {
* via the `definition` "unstored-media".
*/
export interface UnstoredMedia {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -846,7 +846,7 @@ export interface UnstoredMedia {
* via the `definition` "externally-served-media".
*/
export interface ExternallyServedMedia {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -864,8 +864,8 @@ export interface ExternallyServedMedia {
* via the `definition` "uploads-1".
*/
export interface Uploads1 {
id: number;
media?: number | Uploads2 | null;
id: string;
media?: (string | null) | Uploads2;
richText?: {
root: {
type: string;
@@ -898,7 +898,7 @@ export interface Uploads1 {
* via the `definition` "uploads-2".
*/
export interface Uploads2 {
id: number;
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
@@ -917,7 +917,7 @@ export interface Uploads2 {
* via the `definition` "admin-thumbnail-function".
*/
export interface AdminThumbnailFunction {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -935,7 +935,7 @@ export interface AdminThumbnailFunction {
* via the `definition` "admin-thumbnail-size".
*/
export interface AdminThumbnailSize {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -971,7 +971,7 @@ export interface AdminThumbnailSize {
* via the `definition` "optional-file".
*/
export interface OptionalFile {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -989,7 +989,7 @@ export interface OptionalFile {
* via the `definition` "required-file".
*/
export interface RequiredFile {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -1007,7 +1007,7 @@ export interface RequiredFile {
* via the `definition` "custom-upload-field".
*/
export interface CustomUploadField {
id: number;
id: string;
alt?: string | null;
updatedAt: string;
createdAt: string;
@@ -1026,7 +1026,7 @@ export interface CustomUploadField {
* via the `definition` "media-with-relation-preview".
*/
export interface MediaWithRelationPreview {
id: number;
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
@@ -1045,7 +1045,7 @@ export interface MediaWithRelationPreview {
* via the `definition` "media-without-relation-preview".
*/
export interface MediaWithoutRelationPreview {
id: number;
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
@@ -1064,13 +1064,13 @@ export interface MediaWithoutRelationPreview {
* via the `definition` "relation-preview".
*/
export interface RelationPreview {
id: number;
imageWithPreview1?: number | MediaWithRelationPreview | null;
imageWithPreview2?: number | MediaWithRelationPreview | null;
imageWithoutPreview1?: number | MediaWithRelationPreview | null;
imageWithoutPreview2?: number | MediaWithoutRelationPreview | null;
imageWithPreview3?: number | MediaWithoutRelationPreview | null;
imageWithoutPreview3?: number | MediaWithoutRelationPreview | null;
id: string;
imageWithPreview1?: (string | null) | MediaWithRelationPreview;
imageWithPreview2?: (string | null) | MediaWithRelationPreview;
imageWithoutPreview1?: (string | null) | MediaWithRelationPreview;
imageWithoutPreview2?: (string | null) | MediaWithoutRelationPreview;
imageWithPreview3?: (string | null) | MediaWithoutRelationPreview;
imageWithoutPreview3?: (string | null) | MediaWithoutRelationPreview;
updatedAt: string;
createdAt: string;
}
@@ -1079,7 +1079,7 @@ export interface RelationPreview {
* via the `definition` "users".
*/
export interface User {
id: number;
id: string;
updatedAt: string;
createdAt: string;
email: string;
@@ -1096,10 +1096,10 @@ export interface User {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
id: string;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
key?: string | null;
value?:
@@ -1119,7 +1119,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;