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,
|
config: SanitizedConfig,
|
||||||
buildSchemaOptions: BuildSchemaOptions,
|
buildSchemaOptions: BuildSchemaOptions,
|
||||||
): void => {
|
): void => {
|
||||||
const baseSchema = {
|
const hasManyRelations = Array.isArray(field.relationTo)
|
||||||
...formatBaseSchema(field, buildSchemaOptions),
|
let schemaToReturn: { [key: string]: any } = {}
|
||||||
type: mongoose.Schema.Types.Mixed,
|
|
||||||
ref: field.relationTo,
|
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({
|
schema.add({
|
||||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
[field.name]: schemaToReturn,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -717,7 +717,7 @@ export const traverseFields = ({
|
|||||||
case 'upload':
|
case 'upload':
|
||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(field.relationTo)) {
|
||||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
field.relationTo.forEach((relation) => relationships.add(relation))
|
||||||
} else if (field.type === 'relationship' && field.hasMany) {
|
} else if (field.hasMany) {
|
||||||
relationships.add(field.relationTo)
|
relationships.add(field.relationTo)
|
||||||
} else {
|
} else {
|
||||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
// 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 type { Field } from 'payload'
|
||||||
|
|
||||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||||
@@ -34,8 +33,9 @@ export const traverseFields = ({
|
|||||||
// handle simple relationship
|
// handle simple relationship
|
||||||
if (
|
if (
|
||||||
depth > 0 &&
|
depth > 0 &&
|
||||||
(field.type === 'upload' ||
|
(field.type === 'upload' || field.type === 'relationship') &&
|
||||||
(field.type === 'relationship' && !field.hasMany && typeof field.relationTo === 'string'))
|
!field.hasMany &&
|
||||||
|
typeof field.relationTo === 'string'
|
||||||
) {
|
) {
|
||||||
if (field.localized) {
|
if (field.localized) {
|
||||||
_locales.with[`${path}${field.name}`] = true
|
_locales.with[`${path}${field.name}`] = true
|
||||||
|
|||||||
@@ -726,7 +726,7 @@ export const traverseFields = ({
|
|||||||
case 'upload':
|
case 'upload':
|
||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(field.relationTo)) {
|
||||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
field.relationTo.forEach((relation) => relationships.add(relation))
|
||||||
} else if (field.type === 'relationship' && field.hasMany) {
|
} else if (field.hasMany) {
|
||||||
relationships.add(field.relationTo)
|
relationships.add(field.relationTo)
|
||||||
} else {
|
} else {
|
||||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
// 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 'relationship':
|
||||||
case 'upload': {
|
case 'upload': {
|
||||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
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
|
let relationshipFields
|
||||||
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -307,10 +307,51 @@ export function buildMutationInputType({
|
|||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
|
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
|
||||||
}),
|
}),
|
||||||
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => ({
|
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => {
|
||||||
...inputObjectTypeConfig,
|
const { relationTo } = field
|
||||||
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
|
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)
|
const fieldName = formatName(name)
|
||||||
|
|||||||
@@ -594,49 +594,164 @@ export function buildObjectType({
|
|||||||
}),
|
}),
|
||||||
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
|
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
|
||||||
const { relationTo } = field
|
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,
|
// If the relationshipType is undefined at this point,
|
||||||
// it can be assumed that this blockType can have a relationship
|
// it can be assumed that this blockType can have a relationship
|
||||||
// to itself. Therefore, we set the relationshipType equal to the blockType
|
// to itself. Therefore, we set the relationshipType equal to the blockType
|
||||||
// that is currently being created.
|
// that is currently being created.
|
||||||
|
|
||||||
const type = withNullableType(
|
type = type || newlyCreatedBlockType
|
||||||
field,
|
|
||||||
graphqlResult.collections[relationTo].graphQL.type || newlyCreatedBlockType,
|
const relationshipArgs: {
|
||||||
forceNullable,
|
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) {
|
if (config.localization) {
|
||||||
uploadArgs.locale = {
|
relationshipArgs.locale = {
|
||||||
type: graphqlResult.types.localeInputType,
|
type: graphqlResult.types.localeInputType,
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadArgs.fallbackLocale = {
|
relationshipArgs.fallbackLocale = {
|
||||||
type: graphqlResult.types.fallbackLocaleInputType,
|
type: graphqlResult.types.fallbackLocaleInputType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedCollectionSlug = field.relationTo
|
const relationship = {
|
||||||
|
type: withNullableType(
|
||||||
const upload = {
|
field,
|
||||||
type,
|
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||||
args: uploadArgs,
|
forceNullable,
|
||||||
extensions: { complexity: 20 },
|
),
|
||||||
|
args: relationshipArgs,
|
||||||
|
extensions: { complexity: 10 },
|
||||||
async resolve(parent, args, context: Context) {
|
async resolve(parent, args, context: Context) {
|
||||||
const value = parent[field.name]
|
const value = parent[field.name]
|
||||||
const locale = args.locale || context.req.locale
|
const locale = args.locale || context.req.locale
|
||||||
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
||||||
const id = value
|
let relatedCollectionSlug = field.relationTo
|
||||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
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) {
|
if (id) {
|
||||||
const relatedDocument = await context.req.payloadDataLoader.load(
|
const relatedDocument = await context.req.payloadDataLoader.load(
|
||||||
createDataloaderCacheKey({
|
createDataloaderCacheKey({
|
||||||
collectionSlug: relatedCollectionSlug,
|
collectionSlug: relatedCollectionSlug as string,
|
||||||
currentDepth: 0,
|
currentDepth: 0,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
docID: id,
|
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
|
return null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereFields = graphqlResult.collections[relationTo].config.fields
|
|
||||||
|
|
||||||
upload.args.where = {
|
|
||||||
type: buildWhereInputType({
|
|
||||||
name: uploadName,
|
|
||||||
fields: whereFields,
|
|
||||||
parentName: uploadName,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...objectTypeConfig,
|
...objectTypeConfig,
|
||||||
[field.name]: upload,
|
[field.name]: relationship,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,9 +130,36 @@ const fieldToSchemaMap = ({ nestedFieldName, parentName }: Args): any => ({
|
|||||||
textarea: (field: TextareaField) => ({
|
textarea: (field: TextareaField) => ({
|
||||||
type: withOperators(field, parentName),
|
type: withOperators(field, parentName),
|
||||||
}),
|
}),
|
||||||
upload: (field: UploadField) => ({
|
upload: (field: UploadField) => {
|
||||||
type: withOperators(field, parentName),
|
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
|
export default fieldToSchemaMap
|
||||||
|
|||||||
@@ -230,9 +230,9 @@ const defaults: DefaultsType = {
|
|||||||
},
|
},
|
||||||
upload: {
|
upload: {
|
||||||
operators: [
|
operators: [
|
||||||
...operators.equality.map((operator) => ({
|
...[...operators.equality, ...operators.contains].map((operator) => ({
|
||||||
name: 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
|
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||||
|
|
||||||
export const DefaultListView: React.FC = () => {
|
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 { data, defaultLimit, handlePageChange, handlePerPageChange } = useListQuery()
|
||||||
const { searchParams } = useSearchParams()
|
const { searchParams } = useSearchParams()
|
||||||
const { openModal } = useModal()
|
const { openModal } = useModal()
|
||||||
@@ -221,10 +230,15 @@ export const DefaultListView: React.FC = () => {
|
|||||||
<div className={`${baseClass}__list-selection`}>
|
<div className={`${baseClass}__list-selection`}>
|
||||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||||
<div className={`${baseClass}__list-selection-actions`}>
|
<div className={`${baseClass}__list-selection-actions`}>
|
||||||
<EditMany collection={collectionConfig} fields={fields} />
|
{beforeActions && beforeActions}
|
||||||
<PublishMany collection={collectionConfig} />
|
{!disableBulkEdit && (
|
||||||
<UnpublishMany collection={collectionConfig} />
|
<Fragment>
|
||||||
<DeleteMany collection={collectionConfig} />
|
<EditMany collection={collectionConfig} fields={fields} />
|
||||||
|
<PublishMany collection={collectionConfig} />
|
||||||
|
<UnpublishMany collection={collectionConfig} />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ export async function validateSearchParam({
|
|||||||
fieldPath = path.slice(0, -(req.locale.length + 1))
|
fieldPath = path.slice(0, -(req.locale.length + 1))
|
||||||
}
|
}
|
||||||
// remove ".value" from ends of polymorphic relationship paths
|
// 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', '')
|
fieldPath = fieldPath.replace('.value', '')
|
||||||
}
|
}
|
||||||
const entityType: 'collections' | 'globals' = globalConfig ? 'globals' : 'collections'
|
const entityType: 'collections' | 'globals' = globalConfig ? 'globals' : 'collections'
|
||||||
|
|||||||
@@ -99,19 +99,26 @@ export const sanitizeFields = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'relationship') {
|
if (field.min && !field.minRows) {
|
||||||
if (field.min && !field.minRows) {
|
console.warn(
|
||||||
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.`,
|
||||||
`(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))
|
} & 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'>
|
Pick<UIField, 'label' | 'name' | 'type'>
|
||||||
|
|
||||||
export type UploadField = {
|
type SharedUploadProperties = {
|
||||||
admin?: {
|
/**
|
||||||
components?: {
|
* Toggle the preview in the admin interface.
|
||||||
Error?: CustomComponent<UploadFieldErrorClientComponent | UploadFieldErrorServerComponent>
|
*/
|
||||||
Label?: CustomComponent<UploadFieldLabelClientComponent | UploadFieldLabelServerComponent>
|
|
||||||
} & Admin['components']
|
|
||||||
}
|
|
||||||
displayPreview?: boolean
|
displayPreview?: boolean
|
||||||
filterOptions?: FilterOptions
|
filterOptions?: FilterOptions
|
||||||
|
hasMany?: boolean
|
||||||
/**
|
/**
|
||||||
* Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached.
|
* 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}
|
* {@link https://payloadcms.com/docs/getting-started/concepts#field-level-max-depth}
|
||||||
*/
|
*/
|
||||||
maxDepth?: number
|
maxDepth?: number
|
||||||
relationTo: CollectionSlug
|
|
||||||
type: 'upload'
|
type: 'upload'
|
||||||
validate?: Validate<unknown, unknown, unknown, UploadField>
|
validate?: Validate<unknown, unknown, unknown, SharedUploadProperties>
|
||||||
} & FieldBase
|
} & (
|
||||||
|
| {
|
||||||
|
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?: {
|
admin?: {
|
||||||
components?: {
|
sortOptions?: { [collectionSlug: CollectionSlug]: string }
|
||||||
Error?: MappedComponent
|
} & UploadAdmin
|
||||||
Label?: MappedComponent
|
relationTo: CollectionSlug[]
|
||||||
} & AdminClient['components']
|
} & SharedUploadProperties
|
||||||
}
|
|
||||||
} & FieldBaseClient &
|
export type PolymorphicUploadFieldClient = {
|
||||||
Pick<UploadField, 'displayPreview' | 'maxDepth' | 'relationTo' | 'type'>
|
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 = {
|
export type CodeField = {
|
||||||
admin?: {
|
admin?: {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export const promise = async <T>({
|
|||||||
siblingData[field.name] === 'null' ||
|
siblingData[field.name] === 'null' ||
|
||||||
siblingData[field.name] === null
|
siblingData[field.name] === null
|
||||||
) {
|
) {
|
||||||
if (field.type === 'relationship' && field.hasMany === true) {
|
if (field.hasMany === true) {
|
||||||
siblingData[field.name] = []
|
siblingData[field.name] = []
|
||||||
} else {
|
} else {
|
||||||
siblingData[field.name] = null
|
siblingData[field.name] = null
|
||||||
@@ -153,32 +153,32 @@ export const promise = async <T>({
|
|||||||
const relatedCollection = req.payload.config.collections.find(
|
const relatedCollection = req.payload.config.collections.find(
|
||||||
(collection) => collection.slug === relatedDoc.relationTo,
|
(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(
|
const relationshipIDField = relatedCollection.fields.find(
|
||||||
(collectionField) =>
|
(collectionField) =>
|
||||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||||
)
|
)
|
||||||
if (relationshipIDField?.type === 'number') {
|
if (relationshipIDField?.type === 'number') {
|
||||||
siblingData[field.name][i] = {
|
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) }
|
||||||
...relatedDoc,
|
|
||||||
value: parseFloat(relatedDoc.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 {
|
} else {
|
||||||
@@ -187,25 +187,31 @@ export const promise = async <T>({
|
|||||||
const relatedCollection = req.payload.config.collections.find(
|
const relatedCollection = req.payload.config.collections.find(
|
||||||
(collection) => collection.slug === field.relationTo,
|
(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(
|
const relationshipIDField = relatedCollection.fields.find(
|
||||||
(collectionField) =>
|
(collectionField) =>
|
||||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||||
)
|
)
|
||||||
if (relationshipIDField?.type === 'number') {
|
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 type UploadFieldValidation = Validate<unknown, unknown, unknown, UploadField>
|
||||||
export const upload: UploadFieldValidation = (value: string, options) => {
|
|
||||||
if (!value && options.required) {
|
export const upload: UploadFieldValidation = async (value, options) => {
|
||||||
return options?.req?.t('validation:required')
|
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) {
|
if (typeof value !== 'undefined' && value !== null) {
|
||||||
const idType =
|
const values = Array.isArray(value) ? value : [value]
|
||||||
options?.req?.payload?.collections[options.relationTo]?.customIDType ||
|
|
||||||
options?.req?.payload?.db?.defaultIDType
|
|
||||||
|
|
||||||
if (!isValidID(value, idType)) {
|
const invalidRelationships = values.filter((val) => {
|
||||||
return options.req?.t('validation:validUploadID')
|
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
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'upload':
|
||||||
case 'relationship': {
|
case 'relationship': {
|
||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(field.relationTo)) {
|
||||||
if (field.hasMany) {
|
if (field.hasMany) {
|
||||||
@@ -380,21 +381,6 @@ export function fieldsToJSONSchema(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'upload': {
|
|
||||||
fieldSchema = {
|
|
||||||
oneOf: [
|
|
||||||
{
|
|
||||||
type: collectionIDFieldTypes[field.relationTo],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$ref: `#/definitions/${field.relationTo}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
if (!isRequired) fieldSchema.oneOf.push({ type: 'null' })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'blocks': {
|
case 'blocks': {
|
||||||
// Check for a case where no blocks are provided.
|
// 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
|
// 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 {
|
.relationship-add-new {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -10,8 +10,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__add-button,
|
&__add-button {
|
||||||
&__add-button.doc-drawer__toggler {
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add-button--unstyled,
|
||||||
|
&__add-button--unstyled.doc-drawer__toggler {
|
||||||
@include formInput;
|
@include formInput;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
@@ -4,29 +4,30 @@ import type { ClientCollectionConfig } from 'payload'
|
|||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { DocumentInfoContext } from '../../../providers/DocumentInfo/types.js'
|
import type { Value } from '../../fields/Relationship/types.js'
|
||||||
import type { Value } from '../types.js'
|
import type { DocumentInfoContext } from '../../providers/DocumentInfo/types.js'
|
||||||
import type { Props } from './types.js'
|
import type { Props } from './types.js'
|
||||||
|
|
||||||
import { Button } from '../../../elements/Button/index.js'
|
import { PlusIcon } from '../../icons/Plus/index.js'
|
||||||
import { useDocumentDrawer } from '../../../elements/DocumentDrawer/index.js'
|
import { useAuth } from '../../providers/Auth/index.js'
|
||||||
import * as PopupList from '../../../elements/Popup/PopupButtonList/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { Popup } from '../../../elements/Popup/index.js'
|
import { Button } from '../Button/index.js'
|
||||||
import { Tooltip } from '../../../elements/Tooltip/index.js'
|
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
|
||||||
import { PlusIcon } from '../../../icons/Plus/index.js'
|
import * as PopupList from '../Popup/PopupButtonList/index.js'
|
||||||
import { useAuth } from '../../../providers/Auth/index.js'
|
import { Popup } from '../Popup/index.js'
|
||||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
import { Tooltip } from '../Tooltip/index.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { useRelatedCollections } from './useRelatedCollections.js'
|
import { useRelatedCollections } from './useRelatedCollections.js'
|
||||||
|
|
||||||
const baseClass = 'relationship-add-new'
|
const baseClass = 'relationship-add-new'
|
||||||
|
|
||||||
export const AddNewRelation: React.FC<Props> = ({
|
export const AddNewRelation: React.FC<Props> = ({
|
||||||
// dispatchOptions,
|
Button: ButtonFromProps,
|
||||||
hasMany,
|
hasMany,
|
||||||
path,
|
path,
|
||||||
relationTo,
|
relationTo,
|
||||||
setValue,
|
setValue,
|
||||||
|
unstyled,
|
||||||
value,
|
value,
|
||||||
}) => {
|
}) => {
|
||||||
const relatedCollections = useRelatedCollections(relationTo)
|
const relatedCollections = useRelatedCollections(relationTo)
|
||||||
@@ -129,23 +130,34 @@ export const AddNewRelation: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [isDrawerOpen, relatedToMany])
|
}, [isDrawerOpen, relatedToMany])
|
||||||
|
|
||||||
|
const label = t('fields:addNewLabel', {
|
||||||
|
label: getTranslation(relatedCollections[0].labels.singular, i18n),
|
||||||
|
})
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
return (
|
return (
|
||||||
<div className={baseClass} id={`${path}-add-new`}>
|
<div className={baseClass} id={`${path}-add-new`}>
|
||||||
{relatedCollections.length === 1 && (
|
{relatedCollections.length === 1 && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DocumentDrawerToggler
|
<DocumentDrawerToggler
|
||||||
className={`${baseClass}__add-button`}
|
className={[
|
||||||
|
`${baseClass}__add-button`,
|
||||||
|
!unstyled && `${baseClass}__add-button--styled`,
|
||||||
|
].join(' ')}
|
||||||
onClick={() => setShowTooltip(false)}
|
onClick={() => setShowTooltip(false)}
|
||||||
onMouseEnter={() => setShowTooltip(true)}
|
onMouseEnter={() => setShowTooltip(true)}
|
||||||
onMouseLeave={() => setShowTooltip(false)}
|
onMouseLeave={() => setShowTooltip(false)}
|
||||||
>
|
>
|
||||||
<Tooltip className={`${baseClass}__tooltip`} show={showTooltip}>
|
{ButtonFromProps ? (
|
||||||
{t('fields:addNewLabel', {
|
ButtonFromProps
|
||||||
label: getTranslation(relatedCollections[0].labels.singular, i18n),
|
) : (
|
||||||
})}
|
<Fragment>
|
||||||
</Tooltip>
|
<Tooltip className={`${baseClass}__tooltip`} show={showTooltip}>
|
||||||
<PlusIcon />
|
{label}
|
||||||
|
</Tooltip>
|
||||||
|
<PlusIcon />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</DocumentDrawerToggler>
|
</DocumentDrawerToggler>
|
||||||
<DocumentDrawer onSave={onSave} />
|
<DocumentDrawer onSave={onSave} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -154,13 +166,17 @@ export const AddNewRelation: React.FC<Props> = ({
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<Popup
|
<Popup
|
||||||
button={
|
button={
|
||||||
<Button
|
ButtonFromProps ? (
|
||||||
buttonStyle="none"
|
ButtonFromProps
|
||||||
className={`${baseClass}__add-button`}
|
) : (
|
||||||
tooltip={popupOpen ? undefined : t('fields:addNew')}
|
<Button
|
||||||
>
|
buttonStyle="none"
|
||||||
<PlusIcon />
|
className={`${baseClass}__add-button`}
|
||||||
</Button>
|
tooltip={popupOpen ? undefined : t('fields:addNew')}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
buttonType="custom"
|
buttonType="custom"
|
||||||
horizontalAlign="center"
|
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 { useState } from 'react'
|
||||||
|
|
||||||
import { useConfig } from '../../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
|
|
||||||
export const useRelatedCollections = (relationTo: string | string[]): ClientCollectionConfig[] => {
|
export const useRelatedCollections = (relationTo: string | string[]): ClientCollectionConfig[] => {
|
||||||
const { config } = useConfig()
|
const { config } = useConfig()
|
||||||
@@ -6,13 +6,13 @@ import { toast } from 'sonner'
|
|||||||
|
|
||||||
import type { DocumentDrawerProps } from './types.js'
|
import type { DocumentDrawerProps } from './types.js'
|
||||||
|
|
||||||
import { useRelatedCollections } from '../../fields/Relationship/AddNew/useRelatedCollections.js'
|
|
||||||
import { XIcon } from '../../icons/X/index.js'
|
import { XIcon } from '../../icons/X/index.js'
|
||||||
import { RenderComponent } from '../../providers/Config/RenderComponent.js'
|
import { RenderComponent } from '../../providers/Config/RenderComponent.js'
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { DocumentInfoProvider, useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { DocumentInfoProvider, useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
import { useLocale } from '../../providers/Locale/index.js'
|
import { useLocale } from '../../providers/Locale/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
|
import { useRelatedCollections } from '../AddNewRelation/useRelatedCollections.js'
|
||||||
import { Gutter } from '../Gutter/index.js'
|
import { Gutter } from '../Gutter/index.js'
|
||||||
import { IDLabel } from '../IDLabel/index.js'
|
import { IDLabel } from '../IDLabel/index.js'
|
||||||
import { RenderTitle } from '../RenderTitle/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 type { DocumentDrawerProps, DocumentTogglerProps, UseDocumentDrawer } from './types.js'
|
||||||
|
|
||||||
import { useRelatedCollections } from '../../fields/Relationship/AddNew/useRelatedCollections.js'
|
|
||||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
|
import { useRelatedCollections } from '../AddNewRelation/useRelatedCollections.js'
|
||||||
import { Drawer, DrawerToggler } from '../Drawer/index.js'
|
import { Drawer, DrawerToggler } from '../Drawer/index.js'
|
||||||
import { DocumentDrawerContent } from './DrawerContent.js'
|
import { DocumentDrawerContent } from './DrawerContent.js'
|
||||||
import './index.scss'
|
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 {
|
.file-details {
|
||||||
background: var(--theme-elevation-50);
|
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'
|
'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'
|
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
|
collectionSlug: string
|
||||||
customUploadActions?: React.ReactNode[]
|
customUploadActions?: React.ReactNode[]
|
||||||
doc: {
|
doc: {
|
||||||
sizes?: FileSizes
|
sizes?: FileSizes
|
||||||
} & Data
|
} & Data
|
||||||
enableAdjustments?: boolean
|
enableAdjustments?: boolean
|
||||||
handleRemove?: () => void
|
|
||||||
hasImageSizes?: boolean
|
hasImageSizes?: boolean
|
||||||
imageCacheTag?: string
|
imageCacheTag?: string
|
||||||
uploadConfig: SanitizedCollectionConfig['upload']
|
uploadConfig: SanitizedCollectionConfig['upload']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileDetails: React.FC<FileDetailsProps> = (props) => {
|
type StaticFileDetailsProps = {
|
||||||
const {
|
draggableItemProps?: never
|
||||||
collectionSlug,
|
handleRemove?: () => void
|
||||||
customUploadActions,
|
hasMany?: never
|
||||||
doc,
|
isSortable?: never
|
||||||
enableAdjustments,
|
removeItem?: never
|
||||||
handleRemove,
|
rowIndex?: never
|
||||||
hasImageSizes,
|
}
|
||||||
imageCacheTag,
|
|
||||||
uploadConfig,
|
type DraggableFileDetailsProps = {
|
||||||
} = props
|
handleRemove?: never
|
||||||
|
hasMany: boolean
|
||||||
const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc
|
isSortable?: boolean
|
||||||
|
removeItem?: (index: number) => void
|
||||||
return (
|
rowIndex: number
|
||||||
<div className={baseClass}>
|
}
|
||||||
<header>
|
|
||||||
<Thumbnail
|
export type FileDetailsProps = (DraggableFileDetailsProps | StaticFileDetailsProps) &
|
||||||
// size="small"
|
SharedFileDetailsProps
|
||||||
className={`${baseClass}__thumbnail`}
|
|
||||||
collectionSlug={collectionSlug}
|
export const FileDetails: React.FC<FileDetailsProps> = (props) => {
|
||||||
doc={doc}
|
const { hasMany } = props
|
||||||
fileSrc={thumbnailURL || url}
|
|
||||||
imageCacheTag={imageCacheTag}
|
if (hasMany) {
|
||||||
uploadConfig={uploadConfig}
|
return <DraggableFileDetails {...props} />
|
||||||
/>
|
}
|
||||||
<div className={`${baseClass}__main-detail`}>
|
|
||||||
<FileMeta
|
return <StaticFileDetails {...props} />
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { ClientCollectionConfig, ClientField, Where } from 'payload'
|
|||||||
|
|
||||||
import { useWindowInfo } from '@faceless-ui/window-info'
|
import { useWindowInfo } from '@faceless-ui/window-info'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
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'
|
import AnimateHeightImport from 'react-animate-height'
|
||||||
|
|
||||||
const AnimateHeight = (AnimateHeightImport.default ||
|
const AnimateHeight = (AnimateHeightImport.default ||
|
||||||
@@ -49,7 +49,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
|||||||
const { collectionConfig, enableColumns = true, enableSort = false, fields } = props
|
const { collectionConfig, enableColumns = true, enableSort = false, fields } = props
|
||||||
|
|
||||||
const { handleSearchChange } = useListQuery()
|
const { handleSearchChange } = useListQuery()
|
||||||
const { collectionSlug } = useListInfo()
|
const { beforeActions, collectionSlug, disableBulkDelete, disableBulkEdit } = useListInfo()
|
||||||
const { searchParams } = useSearchParams()
|
const { searchParams } = useSearchParams()
|
||||||
const titleField = useUseTitleField(collectionConfig, fields)
|
const titleField = useUseTitleField(collectionConfig, fields)
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
@@ -144,10 +144,15 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
|||||||
<div className={`${baseClass}__buttons-wrap`}>
|
<div className={`${baseClass}__buttons-wrap`}>
|
||||||
{!smallBreak && (
|
{!smallBreak && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<EditMany collection={collectionConfig} fields={fields} />
|
{beforeActions && beforeActions}
|
||||||
<PublishMany collection={collectionConfig} />
|
{!disableBulkEdit && (
|
||||||
<UnpublishMany collection={collectionConfig} />
|
<Fragment>
|
||||||
<DeleteMany collection={collectionConfig} />
|
<EditMany collection={collectionConfig} fields={fields} />
|
||||||
|
<PublishMany collection={collectionConfig} />
|
||||||
|
<UnpublishMany collection={collectionConfig} />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
{enableColumns && (
|
{enableColumns && (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
|||||||
|
|
||||||
import type { ListDrawerProps } from './types.js'
|
import type { ListDrawerProps } from './types.js'
|
||||||
|
|
||||||
|
import { SelectMany } from '../../elements/SelectMany/index.js'
|
||||||
import { FieldLabel } from '../../fields/FieldLabel/index.js'
|
import { FieldLabel } from '../../fields/FieldLabel/index.js'
|
||||||
import { usePayloadAPI } from '../../hooks/usePayloadAPI.js'
|
import { usePayloadAPI } from '../../hooks/usePayloadAPI.js'
|
||||||
import { XIcon } from '../../icons/X/index.js'
|
import { XIcon } from '../../icons/X/index.js'
|
||||||
@@ -45,7 +46,9 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
|||||||
collectionSlugs,
|
collectionSlugs,
|
||||||
customHeader,
|
customHeader,
|
||||||
drawerSlug,
|
drawerSlug,
|
||||||
|
enableRowSelections,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
|
onBulkSelect,
|
||||||
onSelect,
|
onSelect,
|
||||||
selectedCollection,
|
selectedCollection,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -276,8 +279,13 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
}
|
}
|
||||||
|
beforeActions={
|
||||||
|
enableRowSelections ? [<SelectMany key="select-many" onClick={onBulkSelect} />] : undefined
|
||||||
|
}
|
||||||
collectionConfig={selectedCollectionConfig}
|
collectionConfig={selectedCollectionConfig}
|
||||||
collectionSlug={selectedCollectionConfig.slug}
|
collectionSlug={selectedCollectionConfig.slug}
|
||||||
|
disableBulkDelete
|
||||||
|
disableBulkEdit
|
||||||
hasCreatePermission={hasCreatePermission}
|
hasCreatePermission={hasCreatePermission}
|
||||||
newDocumentURL={null}
|
newDocumentURL={null}
|
||||||
>
|
>
|
||||||
@@ -309,6 +317,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
collectionSlug={selectedCollectionConfig.slug}
|
collectionSlug={selectedCollectionConfig.slug}
|
||||||
|
enableRowSelections={enableRowSelections}
|
||||||
preferenceKey={preferenceKey}
|
preferenceKey={preferenceKey}
|
||||||
>
|
>
|
||||||
<RenderComponent mappedComponent={List} />
|
<RenderComponent mappedComponent={List} />
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export const useListDrawer: UseListDrawer = ({
|
|||||||
setCollectionSlugs(filteredCollectionSlugs.map(({ slug }) => slug))
|
setCollectionSlugs(filteredCollectionSlugs.map(({ slug }) => slug))
|
||||||
}
|
}
|
||||||
}, [collectionSlugs, uploads, collections])
|
}, [collectionSlugs, uploads, collections])
|
||||||
|
|
||||||
const toggleDrawer = useCallback(() => {
|
const toggleDrawer = useCallback(() => {
|
||||||
toggleModal(drawerSlug)
|
toggleModal(drawerSlug)
|
||||||
}, [toggleModal, drawerSlug])
|
}, [toggleModal, drawerSlug])
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import type { FilterOptionsResult, SanitizedCollectionConfig } from 'payload'
|
|||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import type { HTMLAttributes } from 'react'
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
import type { useSelection } from '../../providers/Selection/index.js'
|
||||||
|
|
||||||
export type ListDrawerProps = {
|
export type ListDrawerProps = {
|
||||||
readonly collectionSlugs: string[]
|
readonly collectionSlugs: string[]
|
||||||
readonly customHeader?: React.ReactNode
|
readonly customHeader?: React.ReactNode
|
||||||
readonly drawerSlug?: string
|
readonly drawerSlug?: string
|
||||||
|
readonly enableRowSelections?: boolean
|
||||||
readonly filterOptions?: FilterOptionsResult
|
readonly filterOptions?: FilterOptionsResult
|
||||||
|
readonly onBulkSelect?: (selected: ReturnType<typeof useSelection>['selected']) => void
|
||||||
readonly onSelect?: (args: {
|
readonly onSelect?: (args: {
|
||||||
collectionSlug: SanitizedCollectionConfig['slug']
|
collectionSlug: SanitizedCollectionConfig['slug']
|
||||||
docID: string
|
docID: string
|
||||||
@@ -27,7 +31,7 @@ export type UseListDrawer = (args: {
|
|||||||
selectedCollection?: string
|
selectedCollection?: string
|
||||||
uploads?: boolean // finds all collections with upload: true
|
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
|
React.FC<Pick<ListTogglerProps, 'children' | 'className' | 'disabled'>>, // toggler
|
||||||
{
|
{
|
||||||
closeDrawer: () => void
|
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 { DocumentDrawerProps } from '../../elements/DocumentDrawer/types.js'
|
||||||
import type { GetResults, Option, Value } from './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 { ReactSelect } from '../../elements/ReactSelect/index.js'
|
||||||
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
||||||
import { useField } from '../../forms/useField/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 { FieldError } from '../FieldError/index.js'
|
||||||
import { FieldLabel } from '../FieldLabel/index.js'
|
import { FieldLabel } from '../FieldLabel/index.js'
|
||||||
import { fieldBaseClass } from '../shared/index.js'
|
import { fieldBaseClass } from '../shared/index.js'
|
||||||
import { AddNewRelation } from './AddNew/index.js'
|
|
||||||
import { createRelationMap } from './createRelationMap.js'
|
import { createRelationMap } from './createRelationMap.js'
|
||||||
import { findOptionsByValue } from './findOptionsByValue.js'
|
import { findOptionsByValue } from './findOptionsByValue.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -591,15 +591,11 @@ const RelationshipFieldComponent: React.FC<RelationshipFieldProps> = (props) =>
|
|||||||
/>
|
/>
|
||||||
{!readOnly && allowCreate && (
|
{!readOnly && allowCreate && (
|
||||||
<AddNewRelation
|
<AddNewRelation
|
||||||
{...{
|
hasMany={hasMany}
|
||||||
dispatchOptions,
|
path={path}
|
||||||
hasMany,
|
relationTo={relationTo}
|
||||||
options,
|
setValue={setValue}
|
||||||
path,
|
value={value}
|
||||||
relationTo,
|
|
||||||
setValue,
|
|
||||||
value,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 { getTranslation } from '@payloadcms/translations'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { DocumentDrawerProps } from '../../elements/DocumentDrawer/types.js'
|
import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types.js'
|
||||||
import type { ListDrawerProps } from '../../elements/ListDrawer/types.js'
|
import type { ListDrawerProps } from '../../../elements/ListDrawer/types.js'
|
||||||
|
|
||||||
import { Button } from '../../elements/Button/index.js'
|
import { Button } from '../../../elements/Button/index.js'
|
||||||
import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js'
|
import { useDocumentDrawer } from '../../../elements/DocumentDrawer/index.js'
|
||||||
import { FileDetails } from '../../elements/FileDetails/index.js'
|
import { FileDetails } from '../../../elements/FileDetails/index.js'
|
||||||
import { useListDrawer } from '../../elements/ListDrawer/index.js'
|
import { useListDrawer } from '../../../elements/ListDrawer/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||||
import { FieldDescription } from '../FieldDescription/index.js'
|
import { FieldDescription } from '../../FieldDescription/index.js'
|
||||||
import { FieldError } from '../FieldError/index.js'
|
import { FieldError } from '../../FieldError/index.js'
|
||||||
import { FieldLabel } from '../FieldLabel/index.js'
|
import { FieldLabel } from '../../FieldLabel/index.js'
|
||||||
import { fieldBaseClass } from '../shared/index.js'
|
import { fieldBaseClass } from '../../shared/index.js'
|
||||||
|
import { baseClass } from '../index.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'upload'
|
|
||||||
|
|
||||||
export type UploadInputProps = {
|
export type UploadInputProps = {
|
||||||
readonly Description?: MappedComponent
|
readonly Description?: MappedComponent
|
||||||
readonly Error?: MappedComponent
|
readonly Error?: MappedComponent
|
||||||
@@ -63,7 +62,7 @@ export type UploadInputProps = {
|
|||||||
readonly width?: string
|
readonly width?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
export const UploadInputHasOne: React.FC<UploadInputProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
Description,
|
Description,
|
||||||
Error,
|
Error,
|
||||||
@@ -149,7 +148,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
[onChange, closeListDrawer],
|
[onChange, closeListDrawer],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (collection.upload) {
|
if (collection.upload && typeof relationTo === 'string') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../../scss/styles.scss';
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
.upload {
|
.upload {
|
||||||
position: relative;
|
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 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 { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
||||||
import { useField } from '../../forms/useField/index.js'
|
import { useField } from '../../forms/useField/index.js'
|
||||||
import { withCondition } from '../../forms/withCondition/index.js'
|
import { withCondition } from '../../forms/withCondition/index.js'
|
||||||
import { useAuth } from '../../providers/Auth/index.js'
|
import { useAuth } from '../../providers/Auth/index.js'
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { UploadComponentHasMany } from './HasMany/index.js'
|
||||||
import { UploadInput } from './Input.js'
|
import { UploadInputHasOne } from './HasOne/Input.js'
|
||||||
import './index.scss'
|
import { UploadComponentHasOne } from './HasOne/index.js'
|
||||||
|
|
||||||
export { UploadFieldProps, UploadInput }
|
export { UploadFieldProps, UploadInputHasOne as UploadInput }
|
||||||
export type { UploadInputProps }
|
export type { UploadInputProps }
|
||||||
|
|
||||||
|
export const baseClass = 'upload'
|
||||||
|
|
||||||
const UploadComponent: React.FC<UploadFieldProps> = (props) => {
|
const UploadComponent: React.FC<UploadFieldProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
descriptionProps,
|
|
||||||
errorProps,
|
|
||||||
field,
|
|
||||||
field: {
|
field: {
|
||||||
_path: pathFromProps,
|
_path: pathFromProps,
|
||||||
admin: { className, readOnly: readOnlyFromAdmin, style, width } = {},
|
admin: { readOnly: readOnlyFromAdmin } = {},
|
||||||
label,
|
hasMany,
|
||||||
relationTo,
|
relationTo,
|
||||||
required,
|
required,
|
||||||
},
|
},
|
||||||
labelProps,
|
|
||||||
readOnly: readOnlyFromTopLevelProps,
|
readOnly: readOnlyFromTopLevelProps,
|
||||||
validate,
|
validate,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
|
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
|
||||||
|
|
||||||
const {
|
|
||||||
config: {
|
|
||||||
collections,
|
|
||||||
routes: { api: apiRoute },
|
|
||||||
serverURL,
|
|
||||||
},
|
|
||||||
} = useConfig()
|
|
||||||
|
|
||||||
const { permissions } = useAuth()
|
const { permissions } = useAuth()
|
||||||
|
|
||||||
const collection = collections.find((coll) => coll.slug === relationTo)
|
|
||||||
|
|
||||||
const memoizedValidate = useCallback(
|
const memoizedValidate = useCallback(
|
||||||
(value, options) => {
|
(value, options) => {
|
||||||
if (typeof validate === 'function') {
|
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
|
// Checks if the user has permissions to create a new document in the related collection
|
||||||
const canCreate = useMemo(() => {
|
const canCreate = useMemo(() => {
|
||||||
if (permissions?.collections && permissions.collections?.[relationTo]?.create) {
|
if (typeof relationTo === 'string') {
|
||||||
if (permissions.collections[relationTo].create?.permission === true) {
|
if (permissions?.collections && permissions.collections?.[relationTo]?.create) {
|
||||||
return true
|
if (permissions.collections[relationTo].create?.permission === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}, [relationTo, permissions])
|
}, [relationTo, permissions])
|
||||||
|
|
||||||
const { filterOptions, formInitializing, formProcessing, setValue, showError, value } =
|
const fieldHookResult = useField<string | string[]>({
|
||||||
useField<string>({
|
path: pathFromContext ?? pathFromProps,
|
||||||
path: pathFromContext ?? pathFromProps,
|
validate: memoizedValidate,
|
||||||
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(
|
const onChange = useCallback(
|
||||||
(incomingValue) => {
|
(incomingValue) => {
|
||||||
@@ -86,35 +81,31 @@ const UploadComponent: React.FC<UploadFieldProps> = (props) => {
|
|||||||
[setValue],
|
[setValue],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (collection.upload) {
|
if (hasMany) {
|
||||||
return (
|
return (
|
||||||
<UploadInput
|
<UploadComponentHasMany
|
||||||
Description={field?.admin?.components?.Description}
|
{...props}
|
||||||
Error={field?.admin?.components?.Error}
|
canCreate={canCreate}
|
||||||
Label={field?.admin?.components?.Label}
|
disabled={disabled}
|
||||||
allowNewUpload={canCreate}
|
// Note: the below TS error is thrown bc the field hook return result varies based on `hasMany`
|
||||||
api={apiRoute}
|
// @ts-expect-error
|
||||||
className={className}
|
fieldHookResult={fieldHookResult}
|
||||||
collection={collection}
|
|
||||||
descriptionProps={descriptionProps}
|
|
||||||
errorProps={errorProps}
|
|
||||||
filterOptions={filterOptions}
|
|
||||||
label={label}
|
|
||||||
labelProps={labelProps}
|
|
||||||
onChange={onChange}
|
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)
|
export const UploadField = withCondition(UploadComponent)
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'upload':
|
||||||
case 'relationship': {
|
case 'relationship': {
|
||||||
if (field.filterOptions) {
|
if (field.filterOptions) {
|
||||||
if (typeof field.filterOptions === 'object') {
|
if (typeof field.filterOptions === 'object') {
|
||||||
@@ -467,41 +467,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
break
|
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: {
|
default: {
|
||||||
fieldState.value = data[field.name]
|
fieldState.value = data[field.name]
|
||||||
fieldState.initialValue = data[field.name]
|
fieldState.initialValue = data[field.name]
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ export type ColumnPreferences = Pick<Column, 'accessor' | 'active'>[]
|
|||||||
|
|
||||||
export type ListInfoProps = {
|
export type ListInfoProps = {
|
||||||
readonly Header?: React.ReactNode
|
readonly Header?: React.ReactNode
|
||||||
|
readonly beforeActions?: React.ReactNode[]
|
||||||
readonly collectionConfig: ClientConfig['collections'][0]
|
readonly collectionConfig: ClientConfig['collections'][0]
|
||||||
readonly collectionSlug: SanitizedCollectionConfig['slug']
|
readonly collectionSlug: SanitizedCollectionConfig['slug']
|
||||||
|
readonly disableBulkDelete?: boolean
|
||||||
|
readonly disableBulkEdit?: boolean
|
||||||
readonly hasCreatePermission: boolean
|
readonly hasCreatePermission: boolean
|
||||||
readonly newDocumentURL: string
|
readonly newDocumentURL: string
|
||||||
readonly titleField?: FieldAffectingData
|
readonly titleField?: FieldAffectingData
|
||||||
@@ -18,7 +21,10 @@ export type ListInfoProps = {
|
|||||||
|
|
||||||
export type ListInfoContext = {
|
export type ListInfoContext = {
|
||||||
readonly Header?: React.ReactNode
|
readonly Header?: React.ReactNode
|
||||||
|
readonly beforeActions?: React.ReactNode[]
|
||||||
readonly collectionSlug: string
|
readonly collectionSlug: string
|
||||||
|
readonly disableBulkDelete?: boolean
|
||||||
|
readonly disableBulkEdit?: boolean
|
||||||
readonly hasCreatePermission: boolean
|
readonly hasCreatePermission: boolean
|
||||||
readonly newDocumentURL: string
|
readonly newDocumentURL: string
|
||||||
} & ListInfoProps
|
} & ListInfoProps
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export enum SelectAllStatus {
|
|||||||
|
|
||||||
type SelectionContext = {
|
type SelectionContext = {
|
||||||
count: number
|
count: number
|
||||||
|
disableBulkDelete?: boolean
|
||||||
|
disableBulkEdit?: boolean
|
||||||
getQueryParams: (additionalParams?: Where) => string
|
getQueryParams: (additionalParams?: Where) => string
|
||||||
selectAll: SelectAllStatus
|
selectAll: SelectAllStatus
|
||||||
selected: Record<number | string, boolean>
|
selected: Record<number | string, boolean>
|
||||||
@@ -27,10 +29,11 @@ type SelectionContext = {
|
|||||||
const Context = createContext({} as SelectionContext)
|
const Context = createContext({} as SelectionContext)
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
readonly children: React.ReactNode
|
||||||
docs: any[]
|
readonly docs: any[]
|
||||||
totalDocs: number
|
readonly totalDocs: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalDocs }) => {
|
export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalDocs }) => {
|
||||||
const contextRef = useRef({} as SelectionContext)
|
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 UIFields from './collections/UI/index.js'
|
||||||
import Uploads from './collections/Upload/index.js'
|
import Uploads from './collections/Upload/index.js'
|
||||||
import Uploads2 from './collections/Upload2/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 Uploads3 from './collections/Uploads3/index.js'
|
||||||
import TabsWithRichText from './globals/TabsWithRichText.js'
|
import TabsWithRichText from './globals/TabsWithRichText.js'
|
||||||
import { clearAndSeedEverything } from './seed.js'
|
import { clearAndSeedEverything } from './seed.js'
|
||||||
@@ -79,6 +82,9 @@ export const collectionSlugs: CollectionConfig[] = [
|
|||||||
Uploads,
|
Uploads,
|
||||||
Uploads2,
|
Uploads2,
|
||||||
Uploads3,
|
Uploads3,
|
||||||
|
UploadsMulti,
|
||||||
|
UploadsPoly,
|
||||||
|
UploadsMultiPoly,
|
||||||
UIFields,
|
UIFields,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export interface Config {
|
|||||||
uploads: Upload;
|
uploads: Upload;
|
||||||
uploads2: Uploads2;
|
uploads2: Uploads2;
|
||||||
uploads3: Uploads3;
|
uploads3: Uploads3;
|
||||||
|
'uploads-multi': UploadsMulti;
|
||||||
|
'uploads-poly': UploadsPoly;
|
||||||
|
'uploads-multi-poly': UploadsMultiPoly;
|
||||||
'ui-fields': UiField;
|
'ui-fields': UiField;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
@@ -1311,7 +1314,7 @@ export interface TabsField {
|
|||||||
export interface Upload {
|
export interface Upload {
|
||||||
id: string;
|
id: string;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
media?: string | Upload | null;
|
media?: (string | null) | Upload;
|
||||||
richText?: {
|
richText?: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -1346,7 +1349,7 @@ export interface Upload {
|
|||||||
export interface Uploads2 {
|
export interface Uploads2 {
|
||||||
id: string;
|
id: string;
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
media?: string | Uploads2 | null;
|
media?: (string | null) | Uploads2;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -1365,7 +1368,7 @@ export interface Uploads2 {
|
|||||||
*/
|
*/
|
||||||
export interface Uploads3 {
|
export interface Uploads3 {
|
||||||
id: string;
|
id: string;
|
||||||
media?: string | Uploads3 | null;
|
media?: (string | null) | Uploads3;
|
||||||
richText?: {
|
richText?: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -1393,6 +1396,58 @@ export interface Uploads3 {
|
|||||||
focalX?: number | null;
|
focalX?: number | null;
|
||||||
focalY?: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "ui-fields".
|
* via the `definition` "ui-fields".
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ import {
|
|||||||
tabsFieldsSlug,
|
tabsFieldsSlug,
|
||||||
textFieldsSlug,
|
textFieldsSlug,
|
||||||
uiSlug,
|
uiSlug,
|
||||||
|
uploads2Slug,
|
||||||
|
uploadsMulti,
|
||||||
|
uploadsMultiPoly,
|
||||||
|
uploadsPoly,
|
||||||
uploadsSlug,
|
uploadsSlug,
|
||||||
usersSlug,
|
usersSlug,
|
||||||
} from './slugs.js'
|
} from './slugs.js'
|
||||||
@@ -123,6 +127,50 @@ export const seed = async (_payload: Payload) => {
|
|||||||
overrideAccess: true,
|
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 =
|
const formattedID =
|
||||||
_payload.db.defaultIDType === 'number' ? createdArrayDoc.id : `"${createdArrayDoc.id}"`
|
_payload.db.defaultIDType === 'number' ? createdArrayDoc.id : `"${createdArrayDoc.id}"`
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export const textFieldsSlug = 'text-fields'
|
|||||||
export const uploadsSlug = 'uploads'
|
export const uploadsSlug = 'uploads'
|
||||||
export const uploads2Slug = 'uploads2'
|
export const uploads2Slug = 'uploads2'
|
||||||
export const uploads3Slug = 'uploads3'
|
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 uiSlug = 'ui-fields'
|
||||||
|
|
||||||
export const collectionSlugs = [
|
export const collectionSlugs = [
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export interface Config {
|
|||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
};
|
};
|
||||||
db: {
|
db: {
|
||||||
defaultIDType: number;
|
defaultIDType: string;
|
||||||
};
|
};
|
||||||
globals: {};
|
globals: {};
|
||||||
locale: null;
|
locale: null;
|
||||||
@@ -78,9 +78,9 @@ export interface UserAuthOperations {
|
|||||||
* via the `definition` "relation".
|
* via the `definition` "relation".
|
||||||
*/
|
*/
|
||||||
export interface Relation {
|
export interface Relation {
|
||||||
id: number;
|
id: string;
|
||||||
image?: number | Media | null;
|
image?: (string | null) | Media;
|
||||||
versionedImage?: number | Version | null;
|
versionedImage?: (string | null) | Version;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ export interface Relation {
|
|||||||
* via the `definition` "media".
|
* via the `definition` "media".
|
||||||
*/
|
*/
|
||||||
export interface Media {
|
export interface Media {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -229,7 +229,7 @@ export interface Media {
|
|||||||
* via the `definition` "versions".
|
* via the `definition` "versions".
|
||||||
*/
|
*/
|
||||||
export interface Version {
|
export interface Version {
|
||||||
id: number;
|
id: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -249,8 +249,8 @@ export interface Version {
|
|||||||
* via the `definition` "audio".
|
* via the `definition` "audio".
|
||||||
*/
|
*/
|
||||||
export interface Audio {
|
export interface Audio {
|
||||||
id: number;
|
id: string;
|
||||||
audio?: number | Media | null;
|
audio?: (string | null) | Media;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ export interface Audio {
|
|||||||
* via the `definition` "gif-resize".
|
* via the `definition` "gif-resize".
|
||||||
*/
|
*/
|
||||||
export interface GifResize {
|
export interface GifResize {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -295,7 +295,7 @@ export interface GifResize {
|
|||||||
* via the `definition` "filename-compound-index".
|
* via the `definition` "filename-compound-index".
|
||||||
*/
|
*/
|
||||||
export interface FilenameCompoundIndex {
|
export interface FilenameCompoundIndex {
|
||||||
id: number;
|
id: string;
|
||||||
alt?: string | null;
|
alt?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -332,7 +332,7 @@ export interface FilenameCompoundIndex {
|
|||||||
* via the `definition` "no-image-sizes".
|
* via the `definition` "no-image-sizes".
|
||||||
*/
|
*/
|
||||||
export interface NoImageSize {
|
export interface NoImageSize {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -350,7 +350,7 @@ export interface NoImageSize {
|
|||||||
* via the `definition` "object-fit".
|
* via the `definition` "object-fit".
|
||||||
*/
|
*/
|
||||||
export interface ObjectFit {
|
export interface ObjectFit {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -402,7 +402,7 @@ export interface ObjectFit {
|
|||||||
* via the `definition` "with-meta-data".
|
* via the `definition` "with-meta-data".
|
||||||
*/
|
*/
|
||||||
export interface WithMetaDatum {
|
export interface WithMetaDatum {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -430,7 +430,7 @@ export interface WithMetaDatum {
|
|||||||
* via the `definition` "without-meta-data".
|
* via the `definition` "without-meta-data".
|
||||||
*/
|
*/
|
||||||
export interface WithoutMetaDatum {
|
export interface WithoutMetaDatum {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -458,7 +458,7 @@ export interface WithoutMetaDatum {
|
|||||||
* via the `definition` "with-only-jpeg-meta-data".
|
* via the `definition` "with-only-jpeg-meta-data".
|
||||||
*/
|
*/
|
||||||
export interface WithOnlyJpegMetaDatum {
|
export interface WithOnlyJpegMetaDatum {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -486,7 +486,7 @@ export interface WithOnlyJpegMetaDatum {
|
|||||||
* via the `definition` "crop-only".
|
* via the `definition` "crop-only".
|
||||||
*/
|
*/
|
||||||
export interface CropOnly {
|
export interface CropOnly {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -530,7 +530,7 @@ export interface CropOnly {
|
|||||||
* via the `definition` "focal-only".
|
* via the `definition` "focal-only".
|
||||||
*/
|
*/
|
||||||
export interface FocalOnly {
|
export interface FocalOnly {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -574,7 +574,7 @@ export interface FocalOnly {
|
|||||||
* via the `definition` "focal-no-sizes".
|
* via the `definition` "focal-no-sizes".
|
||||||
*/
|
*/
|
||||||
export interface FocalNoSize {
|
export interface FocalNoSize {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -592,7 +592,7 @@ export interface FocalNoSize {
|
|||||||
* via the `definition` "animated-type-media".
|
* via the `definition` "animated-type-media".
|
||||||
*/
|
*/
|
||||||
export interface AnimatedTypeMedia {
|
export interface AnimatedTypeMedia {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -644,7 +644,7 @@ export interface AnimatedTypeMedia {
|
|||||||
* via the `definition` "enlarge".
|
* via the `definition` "enlarge".
|
||||||
*/
|
*/
|
||||||
export interface Enlarge {
|
export interface Enlarge {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -704,7 +704,7 @@ export interface Enlarge {
|
|||||||
* via the `definition` "reduce".
|
* via the `definition` "reduce".
|
||||||
*/
|
*/
|
||||||
export interface Reduce {
|
export interface Reduce {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -756,7 +756,7 @@ export interface Reduce {
|
|||||||
* via the `definition` "media-trim".
|
* via the `definition` "media-trim".
|
||||||
*/
|
*/
|
||||||
export interface MediaTrim {
|
export interface MediaTrim {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -800,7 +800,7 @@ export interface MediaTrim {
|
|||||||
* via the `definition` "custom-file-name-media".
|
* via the `definition` "custom-file-name-media".
|
||||||
*/
|
*/
|
||||||
export interface CustomFileNameMedia {
|
export interface CustomFileNameMedia {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -828,7 +828,7 @@ export interface CustomFileNameMedia {
|
|||||||
* via the `definition` "unstored-media".
|
* via the `definition` "unstored-media".
|
||||||
*/
|
*/
|
||||||
export interface UnstoredMedia {
|
export interface UnstoredMedia {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -846,7 +846,7 @@ export interface UnstoredMedia {
|
|||||||
* via the `definition` "externally-served-media".
|
* via the `definition` "externally-served-media".
|
||||||
*/
|
*/
|
||||||
export interface ExternallyServedMedia {
|
export interface ExternallyServedMedia {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -864,8 +864,8 @@ export interface ExternallyServedMedia {
|
|||||||
* via the `definition` "uploads-1".
|
* via the `definition` "uploads-1".
|
||||||
*/
|
*/
|
||||||
export interface Uploads1 {
|
export interface Uploads1 {
|
||||||
id: number;
|
id: string;
|
||||||
media?: number | Uploads2 | null;
|
media?: (string | null) | Uploads2;
|
||||||
richText?: {
|
richText?: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -898,7 +898,7 @@ export interface Uploads1 {
|
|||||||
* via the `definition` "uploads-2".
|
* via the `definition` "uploads-2".
|
||||||
*/
|
*/
|
||||||
export interface Uploads2 {
|
export interface Uploads2 {
|
||||||
id: number;
|
id: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -917,7 +917,7 @@ export interface Uploads2 {
|
|||||||
* via the `definition` "admin-thumbnail-function".
|
* via the `definition` "admin-thumbnail-function".
|
||||||
*/
|
*/
|
||||||
export interface AdminThumbnailFunction {
|
export interface AdminThumbnailFunction {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -935,7 +935,7 @@ export interface AdminThumbnailFunction {
|
|||||||
* via the `definition` "admin-thumbnail-size".
|
* via the `definition` "admin-thumbnail-size".
|
||||||
*/
|
*/
|
||||||
export interface AdminThumbnailSize {
|
export interface AdminThumbnailSize {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -971,7 +971,7 @@ export interface AdminThumbnailSize {
|
|||||||
* via the `definition` "optional-file".
|
* via the `definition` "optional-file".
|
||||||
*/
|
*/
|
||||||
export interface OptionalFile {
|
export interface OptionalFile {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -989,7 +989,7 @@ export interface OptionalFile {
|
|||||||
* via the `definition` "required-file".
|
* via the `definition` "required-file".
|
||||||
*/
|
*/
|
||||||
export interface RequiredFile {
|
export interface RequiredFile {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -1007,7 +1007,7 @@ export interface RequiredFile {
|
|||||||
* via the `definition` "custom-upload-field".
|
* via the `definition` "custom-upload-field".
|
||||||
*/
|
*/
|
||||||
export interface CustomUploadField {
|
export interface CustomUploadField {
|
||||||
id: number;
|
id: string;
|
||||||
alt?: string | null;
|
alt?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -1026,7 +1026,7 @@ export interface CustomUploadField {
|
|||||||
* via the `definition` "media-with-relation-preview".
|
* via the `definition` "media-with-relation-preview".
|
||||||
*/
|
*/
|
||||||
export interface MediaWithRelationPreview {
|
export interface MediaWithRelationPreview {
|
||||||
id: number;
|
id: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -1045,7 +1045,7 @@ export interface MediaWithRelationPreview {
|
|||||||
* via the `definition` "media-without-relation-preview".
|
* via the `definition` "media-without-relation-preview".
|
||||||
*/
|
*/
|
||||||
export interface MediaWithoutRelationPreview {
|
export interface MediaWithoutRelationPreview {
|
||||||
id: number;
|
id: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -1064,13 +1064,13 @@ export interface MediaWithoutRelationPreview {
|
|||||||
* via the `definition` "relation-preview".
|
* via the `definition` "relation-preview".
|
||||||
*/
|
*/
|
||||||
export interface RelationPreview {
|
export interface RelationPreview {
|
||||||
id: number;
|
id: string;
|
||||||
imageWithPreview1?: number | MediaWithRelationPreview | null;
|
imageWithPreview1?: (string | null) | MediaWithRelationPreview;
|
||||||
imageWithPreview2?: number | MediaWithRelationPreview | null;
|
imageWithPreview2?: (string | null) | MediaWithRelationPreview;
|
||||||
imageWithoutPreview1?: number | MediaWithRelationPreview | null;
|
imageWithoutPreview1?: (string | null) | MediaWithRelationPreview;
|
||||||
imageWithoutPreview2?: number | MediaWithoutRelationPreview | null;
|
imageWithoutPreview2?: (string | null) | MediaWithoutRelationPreview;
|
||||||
imageWithPreview3?: number | MediaWithoutRelationPreview | null;
|
imageWithPreview3?: (string | null) | MediaWithoutRelationPreview;
|
||||||
imageWithoutPreview3?: number | MediaWithoutRelationPreview | null;
|
imageWithoutPreview3?: (string | null) | MediaWithoutRelationPreview;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -1079,7 +1079,7 @@ export interface RelationPreview {
|
|||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
*/
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -1096,10 +1096,10 @@ export interface User {
|
|||||||
* via the `definition` "payload-preferences".
|
* via the `definition` "payload-preferences".
|
||||||
*/
|
*/
|
||||||
export interface PayloadPreference {
|
export interface PayloadPreference {
|
||||||
id: number;
|
id: string;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: number | User;
|
value: string | User;
|
||||||
};
|
};
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
value?:
|
value?:
|
||||||
@@ -1119,7 +1119,7 @@ export interface PayloadPreference {
|
|||||||
* via the `definition` "payload-migrations".
|
* via the `definition` "payload-migrations".
|
||||||
*/
|
*/
|
||||||
export interface PayloadMigration {
|
export interface PayloadMigration {
|
||||||
id: number;
|
id: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
batch?: number | null;
|
batch?: number | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user