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:
@@ -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,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(', ')}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
11
packages/ui/src/elements/AddNewRelation/types.ts
Normal file
11
packages/ui/src/elements/AddNewRelation/types.ts
Normal 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[]
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../scss/styles.scss';
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.file-details {
|
||||
background: var(--theme-elevation-50);
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -87,6 +87,7 @@ export const useListDrawer: UseListDrawer = ({
|
||||
setCollectionSlugs(filteredCollectionSlugs.map(({ slug }) => slug))
|
||||
}
|
||||
}, [collectionSlugs, uploads, collections])
|
||||
|
||||
const toggleDrawer = useCallback(() => {
|
||||
toggleModal(drawerSlug)
|
||||
}, [toggleModal, drawerSlug])
|
||||
|
||||
@@ -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
|
||||
|
||||
32
packages/ui/src/elements/SelectMany/index.tsx
Normal file
32
packages/ui/src/elements/SelectMany/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
47
packages/ui/src/fields/Upload/HasMany/index.scss
Normal file
47
packages/ui/src/fields/Upload/HasMany/index.scss
Normal 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 {
|
||||
}
|
||||
}
|
||||
256
packages/ui/src/fields/Upload/HasMany/index.tsx
Normal file
256
packages/ui/src/fields/Upload/HasMany/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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={[
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../scss/styles.scss';
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.upload {
|
||||
position: relative;
|
||||
76
packages/ui/src/fields/Upload/HasOne/index.tsx
Normal file
76
packages/ui/src/fields/Upload/HasOne/index.tsx
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
1
test/fields/collections/UploadMulti/.gitignore
vendored
Normal file
1
test/fields/collections/UploadMulti/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
uploads
|
||||
21
test/fields/collections/UploadMulti/index.ts
Normal file
21
test/fields/collections/UploadMulti/index.ts
Normal 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
|
||||
5
test/fields/collections/UploadMulti/shared.ts
Normal file
5
test/fields/collections/UploadMulti/shared.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Upload } from '../../payload-types.js'
|
||||
|
||||
export const uploadsDoc: Partial<Upload> = {
|
||||
text: 'An upload here',
|
||||
}
|
||||
1
test/fields/collections/UploadMultiPoly/.gitignore
vendored
Normal file
1
test/fields/collections/UploadMultiPoly/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
uploads
|
||||
21
test/fields/collections/UploadMultiPoly/index.ts
Normal file
21
test/fields/collections/UploadMultiPoly/index.ts
Normal 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
|
||||
5
test/fields/collections/UploadMultiPoly/shared.ts
Normal file
5
test/fields/collections/UploadMultiPoly/shared.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Upload } from '../../payload-types.js'
|
||||
|
||||
export const uploadsDoc: Partial<Upload> = {
|
||||
text: 'An upload here',
|
||||
}
|
||||
1
test/fields/collections/UploadPoly/.gitignore
vendored
Normal file
1
test/fields/collections/UploadPoly/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
uploads
|
||||
20
test/fields/collections/UploadPoly/index.ts
Normal file
20
test/fields/collections/UploadPoly/index.ts
Normal 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
|
||||
5
test/fields/collections/UploadPoly/shared.ts
Normal file
5
test/fields/collections/UploadPoly/shared.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Upload } from '../../payload-types.js'
|
||||
|
||||
export const uploadsDoc: Partial<Upload> = {
|
||||
text: 'An upload here',
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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}"`
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user