diff --git a/packages/db-mongodb/src/models/buildSchema.ts b/packages/db-mongodb/src/models/buildSchema.ts index f713f096a8..566c4ea36a 100644 --- a/packages/db-mongodb/src/models/buildSchema.ts +++ b/packages/db-mongodb/src/models/buildSchema.ts @@ -595,14 +595,77 @@ const fieldToSchemaMap: Record = { 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, }) }, } diff --git a/packages/db-sqlite/src/schema/traverseFields.ts b/packages/db-sqlite/src/schema/traverseFields.ts index 5f6ff8841f..3a1a72bba7 100644 --- a/packages/db-sqlite/src/schema/traverseFields.ts +++ b/packages/db-sqlite/src/schema/traverseFields.ts @@ -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 diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index c8d1534022..94f3c1d13b 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -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 diff --git a/packages/drizzle/src/postgres/schema/traverseFields.ts b/packages/drizzle/src/postgres/schema/traverseFields.ts index 9efa42c546..2b0e325d51 100644 --- a/packages/drizzle/src/postgres/schema/traverseFields.ts +++ b/packages/drizzle/src/postgres/schema/traverseFields.ts @@ -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 diff --git a/packages/drizzle/src/queries/getTableColumnFromPath.ts b/packages/drizzle/src/queries/getTableColumnFromPath.ts index 822173d7e9..f311ad1d81 100644 --- a/packages/drizzle/src/queries/getTableColumnFromPath.ts +++ b/packages/drizzle/src/queries/getTableColumnFromPath.ts @@ -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 { diff --git a/packages/graphql/src/schema/buildMutationInputType.ts b/packages/graphql/src/schema/buildMutationInputType.ts index 1260b6883c..87b31e7a7c 100644 --- a/packages/graphql/src/schema/buildMutationInputType.ts +++ b/packages/graphql/src/schema/buildMutationInputType.ts @@ -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 + 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) diff --git a/packages/graphql/src/schema/buildObjectType.ts b/packages/graphql/src/schema/buildObjectType.ts index e17d87aab2..bc1d6cf9a4 100644 --- a/packages/graphql/src/schema/buildObjectType.ts +++ b/packages/graphql/src/schema/buildObjectType.ts @@ -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, } }, } diff --git a/packages/graphql/src/schema/fieldToWhereInputSchemaMap.ts b/packages/graphql/src/schema/fieldToWhereInputSchemaMap.ts index 69b0f3da5d..c0378c9808 100644 --- a/packages/graphql/src/schema/fieldToWhereInputSchemaMap.ts +++ b/packages/graphql/src/schema/fieldToWhereInputSchemaMap.ts @@ -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 diff --git a/packages/graphql/src/schema/withOperators.ts b/packages/graphql/src/schema/withOperators.ts index f1e7b9dcdb..71f9ad60f3 100644 --- a/packages/graphql/src/schema/withOperators.ts +++ b/packages/graphql/src/schema/withOperators.ts @@ -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, })), ], }, diff --git a/packages/next/src/views/List/Default/index.tsx b/packages/next/src/views/List/Default/index.tsx index 1391ffd249..51f6287259 100644 --- a/packages/next/src/views/List/Default/index.tsx +++ b/packages/next/src/views/List/Default/index.tsx @@ -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 = () => {
- - - - + {beforeActions && beforeActions} + {!disableBulkEdit && ( + + + + + + )} + {!disableBulkDelete && }
)} diff --git a/packages/payload/src/database/queryValidation/validateSearchParams.ts b/packages/payload/src/database/queryValidation/validateSearchParams.ts index f188458344..5433e6685a 100644 --- a/packages/payload/src/database/queryValidation/validateSearchParams.ts +++ b/packages/payload/src/database/queryValidation/validateSearchParams.ts @@ -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' diff --git a/packages/payload/src/fields/config/sanitize.ts b/packages/payload/src/fields/config/sanitize.ts index d4c213d40e..dbbb41bea7 100644 --- a/packages/payload/src/fields/config/sanitize.ts +++ b/packages/payload/src/fields/config/sanitize.ts @@ -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 } } diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index fe80667b3e..10a6386e32 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -824,35 +824,106 @@ export type UIFieldClient = { } & Omit, '_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 -export type UploadField = { - admin?: { - components?: { - Error?: CustomComponent - Label?: CustomComponent - } & 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 -} & FieldBase + validate?: Validate +} & ( + | { + 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 + +export type PolymorphicUploadField = { admin?: { - components?: { - Error?: MappedComponent - Label?: MappedComponent - } & AdminClient['components'] - } -} & FieldBaseClient & - Pick + sortOptions?: { [collectionSlug: CollectionSlug]: string } + } & UploadAdmin + relationTo: CollectionSlug[] +} & SharedUploadProperties + +export type PolymorphicUploadFieldClient = { + admin?: { + sortOptions?: Pick + } & UploadAdminClient +} & Pick & + SharedUploadPropertiesClient + +export type SingleUploadField = { + admin?: { + sortOptions?: string + } & UploadAdmin + relationTo: CollectionSlug +} & SharedUploadProperties + +export type SingleUploadFieldClient = { + admin?: Pick & UploadAdminClient +} & Pick & + SharedUploadPropertiesClient + +export type UploadField = /* PolymorphicUploadField | */ SingleUploadField + +export type UploadFieldClient = /* PolymorphicUploadFieldClient | */ SingleUploadFieldClient export type CodeField = { admin?: { diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index 35e6f4ad44..f1d74e81d9 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -138,7 +138,7 @@ export const promise = async ({ 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 ({ 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 ({ 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) } } } diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index 8cc3f2dde5..f58c2ab1d7 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -571,18 +571,76 @@ const validateFilterOptions: Validate< } export type UploadFieldValidation = Validate -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(', ')}` } } diff --git a/packages/payload/src/utilities/configToJSONSchema.ts b/packages/payload/src/utilities/configToJSONSchema.ts index 006a53582a..d7f0d821c3 100644 --- a/packages/payload/src/utilities/configToJSONSchema.ts +++ b/packages/payload/src/utilities/configToJSONSchema.ts @@ -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 diff --git a/packages/ui/src/fields/Relationship/AddNew/index.scss b/packages/ui/src/elements/AddNewRelation/index.scss similarity index 74% rename from packages/ui/src/fields/Relationship/AddNew/index.scss rename to packages/ui/src/elements/AddNewRelation/index.scss index 3e3bd731ef..98c42f785f 100644 --- a/packages/ui/src/fields/Relationship/AddNew/index.scss +++ b/packages/ui/src/elements/AddNewRelation/index.scss @@ -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; diff --git a/packages/ui/src/fields/Relationship/AddNew/index.tsx b/packages/ui/src/elements/AddNewRelation/index.tsx similarity index 78% rename from packages/ui/src/fields/Relationship/AddNew/index.tsx rename to packages/ui/src/elements/AddNewRelation/index.tsx index d9318081dd..e06803b9e5 100644 --- a/packages/ui/src/fields/Relationship/AddNew/index.tsx +++ b/packages/ui/src/elements/AddNewRelation/index.tsx @@ -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 = ({ - // dispatchOptions, + Button: ButtonFromProps, hasMany, path, relationTo, setValue, + unstyled, value, }) => { const relatedCollections = useRelatedCollections(relationTo) @@ -129,23 +130,34 @@ export const AddNewRelation: React.FC = ({ } }, [isDrawerOpen, relatedToMany]) + const label = t('fields:addNewLabel', { + label: getTranslation(relatedCollections[0].labels.singular, i18n), + }) + if (show) { return (
{relatedCollections.length === 1 && ( setShowTooltip(false)} onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} > - - {t('fields:addNewLabel', { - label: getTranslation(relatedCollections[0].labels.singular, i18n), - })} - - + {ButtonFromProps ? ( + ButtonFromProps + ) : ( + + + {label} + + + + )} @@ -154,13 +166,17 @@ export const AddNewRelation: React.FC = ({ - - + ButtonFromProps ? ( + ButtonFromProps + ) : ( + + ) } buttonType="custom" horizontalAlign="center" diff --git a/packages/ui/src/elements/AddNewRelation/types.ts b/packages/ui/src/elements/AddNewRelation/types.ts new file mode 100644 index 0000000000..11be593804 --- /dev/null +++ b/packages/ui/src/elements/AddNewRelation/types.ts @@ -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[] +} diff --git a/packages/ui/src/fields/Relationship/AddNew/useRelatedCollections.ts b/packages/ui/src/elements/AddNewRelation/useRelatedCollections.ts similarity index 90% rename from packages/ui/src/fields/Relationship/AddNew/useRelatedCollections.ts rename to packages/ui/src/elements/AddNewRelation/useRelatedCollections.ts index 82997a8ca3..0667b5408c 100644 --- a/packages/ui/src/fields/Relationship/AddNew/useRelatedCollections.ts +++ b/packages/ui/src/elements/AddNewRelation/useRelatedCollections.ts @@ -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() diff --git a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx index 1fcbe01cb4..394e25a9a0 100644 --- a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx @@ -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' diff --git a/packages/ui/src/elements/DocumentDrawer/index.tsx b/packages/ui/src/elements/DocumentDrawer/index.tsx index de34cae613..b9bae4d5a0 100644 --- a/packages/ui/src/elements/DocumentDrawer/index.tsx +++ b/packages/ui/src/elements/DocumentDrawer/index.tsx @@ -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' diff --git a/packages/ui/src/elements/FileDetails/DraggableFileDetails/index.scss b/packages/ui/src/elements/FileDetails/DraggableFileDetails/index.scss new file mode 100644 index 0000000000..1aa27226e3 --- /dev/null +++ b/packages/ui/src/elements/FileDetails/DraggableFileDetails/index.scss @@ -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; + } +} diff --git a/packages/ui/src/elements/FileDetails/DraggableFileDetails/index.tsx b/packages/ui/src/elements/FileDetails/DraggableFileDetails/index.tsx new file mode 100644 index 0000000000..4a8f1cfb08 --- /dev/null +++ b/packages/ui/src/elements/FileDetails/DraggableFileDetails/index.tsx @@ -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 = (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 ( + + {(draggableSortableItemProps) => ( +
+
+ {isSortable && draggableSortableItemProps && ( +
+ +
+ )} + +
+
{filename}
+ +
+ + + + + {removeItem && ( +
+
+ )} +
+ ) +} diff --git a/packages/ui/src/elements/FileDetails/index.scss b/packages/ui/src/elements/FileDetails/StaticFileDetails/index.scss similarity index 98% rename from packages/ui/src/elements/FileDetails/index.scss rename to packages/ui/src/elements/FileDetails/StaticFileDetails/index.scss index aa409a5938..ee7f61b7c2 100644 --- a/packages/ui/src/elements/FileDetails/index.scss +++ b/packages/ui/src/elements/FileDetails/StaticFileDetails/index.scss @@ -1,4 +1,4 @@ -@import '../../scss/styles.scss'; +@import '../../../scss/styles.scss'; .file-details { background: var(--theme-elevation-50); diff --git a/packages/ui/src/elements/FileDetails/StaticFileDetails/index.tsx b/packages/ui/src/elements/FileDetails/StaticFileDetails/index.tsx new file mode 100644 index 0000000000..5664ace1a0 --- /dev/null +++ b/packages/ui/src/elements/FileDetails/StaticFileDetails/index.tsx @@ -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 = (props) => { + const { + collectionSlug, + customUploadActions, + doc, + enableAdjustments, + handleRemove, + hasImageSizes, + imageCacheTag, + uploadConfig, + } = props + + const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc + + return ( +
+
+ +
+ + + {(enableAdjustments || customUploadActions) && ( + + )} +
+ {handleRemove && ( +
+
+ ) +} diff --git a/packages/ui/src/elements/FileDetails/index.tsx b/packages/ui/src/elements/FileDetails/index.tsx index 7272352326..9f0954c9ac 100644 --- a/packages/ui/src/elements/FileDetails/index.tsx +++ b/packages/ui/src/elements/FileDetails/index.tsx @@ -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 = (props) => { - const { - collectionSlug, - customUploadActions, - doc, - enableAdjustments, - handleRemove, - hasImageSizes, - imageCacheTag, - uploadConfig, - } = props - - const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc - - return ( -
-
- -
- - - {(enableAdjustments || customUploadActions) && ( - - )} -
- {handleRemove && ( -
-
- ) +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 = (props) => { + const { hasMany } = props + + if (hasMany) { + return + } + + return } diff --git a/packages/ui/src/elements/ListControls/index.tsx b/packages/ui/src/elements/ListControls/index.tsx index 8ac1917fc0..892f75f913 100644 --- a/packages/ui/src/elements/ListControls/index.tsx +++ b/packages/ui/src/elements/ListControls/index.tsx @@ -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 = (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 = (props) => {
{!smallBreak && ( - - - - + {beforeActions && beforeActions} + {!disableBulkEdit && ( + + + + + + )} + {!disableBulkDelete && } )} {enableColumns && ( diff --git a/packages/ui/src/elements/ListDrawer/DrawerContent.tsx b/packages/ui/src/elements/ListDrawer/DrawerContent.tsx index 43c4698773..894971734c 100644 --- a/packages/ui/src/elements/ListDrawer/DrawerContent.tsx +++ b/packages/ui/src/elements/ListDrawer/DrawerContent.tsx @@ -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 = ({ collectionSlugs, customHeader, drawerSlug, + enableRowSelections, filterOptions, + onBulkSelect, onSelect, selectedCollection, }) => { @@ -276,8 +279,13 @@ export const ListDrawerContent: React.FC = ({ )} } + beforeActions={ + enableRowSelections ? [] : undefined + } collectionConfig={selectedCollectionConfig} collectionSlug={selectedCollectionConfig.slug} + disableBulkDelete + disableBulkEdit hasCreatePermission={hasCreatePermission} newDocumentURL={null} > @@ -309,6 +317,7 @@ export const ListDrawerContent: React.FC = ({ }, ]} collectionSlug={selectedCollectionConfig.slug} + enableRowSelections={enableRowSelections} preferenceKey={preferenceKey} > diff --git a/packages/ui/src/elements/ListDrawer/index.tsx b/packages/ui/src/elements/ListDrawer/index.tsx index 954b716f04..76b9c100de 100644 --- a/packages/ui/src/elements/ListDrawer/index.tsx +++ b/packages/ui/src/elements/ListDrawer/index.tsx @@ -87,6 +87,7 @@ export const useListDrawer: UseListDrawer = ({ setCollectionSlugs(filteredCollectionSlugs.map(({ slug }) => slug)) } }, [collectionSlugs, uploads, collections]) + const toggleDrawer = useCallback(() => { toggleModal(drawerSlug) }, [toggleModal, drawerSlug]) diff --git a/packages/ui/src/elements/ListDrawer/types.ts b/packages/ui/src/elements/ListDrawer/types.ts index dfe55c54f0..d567632177 100644 --- a/packages/ui/src/elements/ListDrawer/types.ts +++ b/packages/ui/src/elements/ListDrawer/types.ts @@ -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['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>, // drawer + React.FC>, // drawer React.FC>, // toggler { closeDrawer: () => void diff --git a/packages/ui/src/elements/SelectMany/index.tsx b/packages/ui/src/elements/SelectMany/index.tsx new file mode 100644 index 0000000000..aef666d6ef --- /dev/null +++ b/packages/ui/src/elements/SelectMany/index.tsx @@ -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['selected']) => void +}> = (props) => { + const { onClick } = props + + const { count, selected } = useSelection() + // const { t } = useTranslation() + + if (!selected || !count) { + return null + } + + return ( + { + if (typeof onClick === 'function') { + onClick(selected) + } + }} + pillStyle="white" + > + {`Select ${count}`} + + ) +} diff --git a/packages/ui/src/fields/Relationship/AddNew/types.ts b/packages/ui/src/fields/Relationship/AddNew/types.ts deleted file mode 100644 index 6b0a97133c..0000000000 --- a/packages/ui/src/fields/Relationship/AddNew/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type React from 'react' - -import type { Action, OptionGroup, Value } from '../types.js' - -export type Props = { - dispatchOptions: React.Dispatch - hasMany: boolean - options: OptionGroup[] - path: string - relationTo: string | string[] - setValue: (value: unknown) => void - value: Value | Value[] -} diff --git a/packages/ui/src/fields/Relationship/index.tsx b/packages/ui/src/fields/Relationship/index.tsx index ae34fc6938..cd39968025 100644 --- a/packages/ui/src/fields/Relationship/index.tsx +++ b/packages/ui/src/fields/Relationship/index.tsx @@ -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 = (props) => /> {!readOnly && allowCreate && ( )}
diff --git a/packages/ui/src/fields/Upload/HasMany/index.scss b/packages/ui/src/fields/Upload/HasMany/index.scss new file mode 100644 index 0000000000..c09b590b07 --- /dev/null +++ b/packages/ui/src/fields/Upload/HasMany/index.scss @@ -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 { + } +} diff --git a/packages/ui/src/fields/Upload/HasMany/index.tsx b/packages/ui/src/fields/Upload/HasMany/index.tsx new file mode 100644 index 0000000000..02d2afcb7a --- /dev/null +++ b/packages/ui/src/fields/Upload/HasMany/index.tsx @@ -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> = (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(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['selected']) => { + const selectedIDs = Object.entries(selections).reduce( + (acc, [key, value]) => (value ? [...acc, key] : acc), + [] as string[], + ) + setValue([...value, ...selectedIDs]) + }, + [setValue, value], + ) + + return ( + +
+ + +
+
+ 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 ( + + ) + })} + +
+
+ +
+
+ {canCreate && ( +
+ + {t('fields:addNew')} + + } + hasMany={hasMany} + path={_path} + relationTo={relationTo} + setValue={setValue} + unstyled + value={value} + /> +
+ )} + +
+ +
+
+
+ +
+
+ { + setValue([...value, selection.docID]) + }} + /> +
+ ) +} diff --git a/packages/ui/src/fields/Upload/Input.tsx b/packages/ui/src/fields/Upload/HasOne/Input.tsx similarity index 87% rename from packages/ui/src/fields/Upload/Input.tsx rename to packages/ui/src/fields/Upload/HasOne/Input.tsx index 9bf3f2def0..87d78d9f3f 100644 --- a/packages/ui/src/fields/Upload/Input.tsx +++ b/packages/ui/src/fields/Upload/HasOne/Input.tsx @@ -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 = (props) => { +export const UploadInputHasOne: React.FC = (props) => { const { Description, Error, @@ -149,7 +148,7 @@ export const UploadInput: React.FC = (props) => { [onChange, closeListDrawer], ) - if (collection.upload) { + if (collection.upload && typeof relationTo === 'string') { return (
= { + readonly canCreate: boolean + readonly disabled: boolean + readonly fieldHookResult: ReturnType> + readonly onChange: (value: unknown) => void +} & UploadFieldProps + +export const UploadComponentHasOne: React.FC = (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 ( + + ) + } + } else { + return
Polymorphic Has One Uploads Go Here
+ } + + return null +} diff --git a/packages/ui/src/fields/Upload/index.tsx b/packages/ui/src/fields/Upload/index.tsx index 7b46b41fa0..db76d081e1 100644 --- a/packages/ui/src/fields/Upload/index.tsx +++ b/packages/ui/src/fields/Upload/index.tsx @@ -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 = (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 = (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({ - path: pathFromContext ?? pathFromProps, - validate: memoizedValidate, - }) + const fieldHookResult = useField({ + 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 = (props) => { [setValue], ) - if (collection.upload) { + if (hasMany) { return ( - ) } - return null + return ( + + ) } export const UploadField = withCondition(UploadComponent) diff --git a/packages/ui/src/forms/buildStateFromSchema/addFieldStatePromise.ts b/packages/ui/src/forms/buildStateFromSchema/addFieldStatePromise.ts index 42fba6b24e..1fec00475e 100644 --- a/packages/ui/src/forms/buildStateFromSchema/addFieldStatePromise.ts +++ b/packages/ui/src/forms/buildStateFromSchema/addFieldStatePromise.ts @@ -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] diff --git a/packages/ui/src/providers/ListInfo/index.tsx b/packages/ui/src/providers/ListInfo/index.tsx index 79c1002fc8..a4acab574a 100644 --- a/packages/ui/src/providers/ListInfo/index.tsx +++ b/packages/ui/src/providers/ListInfo/index.tsx @@ -9,8 +9,11 @@ export type ColumnPreferences = Pick[] 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 diff --git a/packages/ui/src/providers/Selection/index.tsx b/packages/ui/src/providers/Selection/index.tsx index 3d1f3649f6..4b3983eda8 100644 --- a/packages/ui/src/providers/Selection/index.tsx +++ b/packages/ui/src/providers/Selection/index.tsx @@ -16,6 +16,8 @@ export enum SelectAllStatus { type SelectionContext = { count: number + disableBulkDelete?: boolean + disableBulkEdit?: boolean getQueryParams: (additionalParams?: Where) => string selectAll: SelectAllStatus selected: Record @@ -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 = ({ children, docs = [], totalDocs }) => { const contextRef = useRef({} as SelectionContext) diff --git a/test/fields/collections/UploadMulti/.gitignore b/test/fields/collections/UploadMulti/.gitignore new file mode 100644 index 0000000000..3f549faf91 --- /dev/null +++ b/test/fields/collections/UploadMulti/.gitignore @@ -0,0 +1 @@ +uploads diff --git a/test/fields/collections/UploadMulti/index.ts b/test/fields/collections/UploadMulti/index.ts new file mode 100644 index 0000000000..e0438365d4 --- /dev/null +++ b/test/fields/collections/UploadMulti/index.ts @@ -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 diff --git a/test/fields/collections/UploadMulti/shared.ts b/test/fields/collections/UploadMulti/shared.ts new file mode 100644 index 0000000000..aa1f5c70dc --- /dev/null +++ b/test/fields/collections/UploadMulti/shared.ts @@ -0,0 +1,5 @@ +import type { Upload } from '../../payload-types.js' + +export const uploadsDoc: Partial = { + text: 'An upload here', +} diff --git a/test/fields/collections/UploadMultiPoly/.gitignore b/test/fields/collections/UploadMultiPoly/.gitignore new file mode 100644 index 0000000000..3f549faf91 --- /dev/null +++ b/test/fields/collections/UploadMultiPoly/.gitignore @@ -0,0 +1 @@ +uploads diff --git a/test/fields/collections/UploadMultiPoly/index.ts b/test/fields/collections/UploadMultiPoly/index.ts new file mode 100644 index 0000000000..00dfbace15 --- /dev/null +++ b/test/fields/collections/UploadMultiPoly/index.ts @@ -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 diff --git a/test/fields/collections/UploadMultiPoly/shared.ts b/test/fields/collections/UploadMultiPoly/shared.ts new file mode 100644 index 0000000000..aa1f5c70dc --- /dev/null +++ b/test/fields/collections/UploadMultiPoly/shared.ts @@ -0,0 +1,5 @@ +import type { Upload } from '../../payload-types.js' + +export const uploadsDoc: Partial = { + text: 'An upload here', +} diff --git a/test/fields/collections/UploadPoly/.gitignore b/test/fields/collections/UploadPoly/.gitignore new file mode 100644 index 0000000000..3f549faf91 --- /dev/null +++ b/test/fields/collections/UploadPoly/.gitignore @@ -0,0 +1 @@ +uploads diff --git a/test/fields/collections/UploadPoly/index.ts b/test/fields/collections/UploadPoly/index.ts new file mode 100644 index 0000000000..d53c3e5a10 --- /dev/null +++ b/test/fields/collections/UploadPoly/index.ts @@ -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 diff --git a/test/fields/collections/UploadPoly/shared.ts b/test/fields/collections/UploadPoly/shared.ts new file mode 100644 index 0000000000..aa1f5c70dc --- /dev/null +++ b/test/fields/collections/UploadPoly/shared.ts @@ -0,0 +1,5 @@ +import type { Upload } from '../../payload-types.js' + +export const uploadsDoc: Partial = { + text: 'An upload here', +} diff --git a/test/fields/config.ts b/test/fields/config.ts index e819672e6f..7c5b7849fd 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -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, ] diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 192ae24f4d..dffca67267 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -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". diff --git a/test/fields/seed.ts b/test/fields/seed.ts index 972414e513..59fbda67f0 100644 --- a/test/fields/seed.ts +++ b/test/fields/seed.ts @@ -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}"` diff --git a/test/fields/slugs.ts b/test/fields/slugs.ts index 1fa9de9b75..53258eed69 100644 --- a/test/fields/slugs.ts +++ b/test/fields/slugs.ts @@ -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 = [ diff --git a/test/uploads/payload-types.ts b/test/uploads/payload-types.ts index bb6d267390..6cad1213a8 100644 --- a/test/uploads/payload-types.ts +++ b/test/uploads/payload-types.ts @@ -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;