feat: join field across many collections (#10919)
This feature allows you to specify `collection` for the join field as
array.
This can be useful for example to describe relationship linking like
this:
```ts
{
slug: 'folders',
fields: [
{
type: 'join',
on: 'folder',
collection: ['files', 'documents', 'folders'],
name: 'children',
},
{
type: 'relationship',
relationTo: 'folders',
name: 'folder',
},
],
},
{
slug: 'files',
upload: true,
fields: [
{
type: 'relationship',
relationTo: 'folders',
name: 'folder',
},
],
},
{
slug: 'documents',
fields: [
{
type: 'relationship',
relationTo: 'folders',
name: 'folder',
},
],
},
```
Documents and files can be placed to folders and folders themselves can
be nested to other folders (root folders just have `folder` as `null`).
Output type of `Folder`:
```ts
export interface Folder {
id: string;
children?: {
docs?:
| (
| {
relationTo?: 'files';
value: string | File;
}
| {
relationTo?: 'documents';
value: string | Document;
}
| {
relationTo?: 'folders';
value: string | Folder;
}
)[]
| null;
hasNextPage?: boolean | null;
} | null;
folder?: (string | null) | Folder;
updatedAt: string;
createdAt: string;
}
```
While you could instead have many join fields (for example
`childrenFolders`, `childrenFiles`) etc - this doesn't allow you to
sort/filter and paginate things across many collections, which isn't
trivial. With SQL we use `UNION ALL` query to achieve that.
---------
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -19,6 +19,7 @@
|
||||
// Load .git-blame-ignore-revs file
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
||||
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||
"jestrunner.changeDirectoryToWorkspaceRoot": false,
|
||||
"jestrunner.debugOptions": {
|
||||
"runtimeArgs": ["--no-deprecation"]
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import { flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const count: Count = async function count(
|
||||
@@ -23,9 +24,11 @@ export const count: Count = async function count(
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { CountOptions } from 'mongodb'
|
||||
import type { CountGlobalVersions } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
@@ -23,9 +24,14 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.globals.config.find((each) => each.slug === global),
|
||||
true,
|
||||
),
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { CountOptions } from 'mongodb'
|
||||
import type { CountVersions } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
@@ -23,9 +24,14 @@ export const countVersions: CountVersions = async function countVersions(
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { DeleteMany } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteMany: DeleteMany = async function deleteMany(
|
||||
@@ -14,8 +15,10 @@ export const deleteMany: DeleteMany = async function deleteMany(
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
payload: this.payload,
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { DeleteOne, Document } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
@@ -21,8 +22,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
payload: this.payload,
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DeleteVersions } from 'payload'
|
||||
import { buildVersionCollectionFields, type DeleteVersions } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteVersions: DeleteVersions = async function deleteVersions(
|
||||
@@ -12,9 +13,14 @@ export const deleteVersions: DeleteVersions = async function deleteVersions(
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await VersionsModel.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
@@ -50,9 +51,11 @@ export const find: Find = async function find(
|
||||
})
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { combineQueries } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
@@ -14,20 +15,22 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
||||
{ slug, locale, req, select, where },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
|
||||
const options: QueryOptions = {
|
||||
lean: true,
|
||||
select: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields,
|
||||
fields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
globalSlug: slug,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: combineQueries({ globalType: { equals: slug } }, where),
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
@@ -46,10 +47,10 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
})
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
globalSlug: global,
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: versionFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Document, FindOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
@@ -20,9 +21,11 @@ export const findOne: FindOne = async function findOne(
|
||||
session,
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
@@ -41,9 +42,12 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
})
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -58,7 +62,7 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
pagination,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
fields,
|
||||
select,
|
||||
}),
|
||||
sort,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { CollectionModel } from './types.js'
|
||||
import { buildCollectionSchema } from './models/buildCollectionSchema.js'
|
||||
import { buildGlobalModel } from './models/buildGlobalModel.js'
|
||||
import { buildSchema } from './models/buildSchema.js'
|
||||
import { getBuildQueryPlugin } from './queries/buildQuery.js'
|
||||
import { getBuildQueryPlugin } from './queries/getBuildQueryPlugin.js'
|
||||
import { getDBName } from './utilities/getDBName.js'
|
||||
|
||||
export const init: Init = function init(this: MongooseAdapter) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Payload, SanitizedCollectionConfig } from 'payload'
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||
import paginate from 'mongoose-paginate-v2'
|
||||
|
||||
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
|
||||
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
|
||||
import { buildSchema } from './buildSchema.js'
|
||||
|
||||
export const buildCollectionSchema = (
|
||||
@@ -44,7 +44,10 @@ export const buildCollectionSchema = (
|
||||
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
|
||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
|
||||
|
||||
if (Object.keys(collection.joins).length > 0) {
|
||||
if (
|
||||
Object.keys(collection.joins).length > 0 ||
|
||||
Object.keys(collection.polymorphicJoins).length > 0
|
||||
) {
|
||||
schema.plugin(mongooseAggregatePaginate)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import mongoose from 'mongoose'
|
||||
|
||||
import type { GlobalModel } from '../types.js'
|
||||
|
||||
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
|
||||
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
|
||||
import { buildSchema } from './buildSchema.js'
|
||||
|
||||
export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
|
||||
|
||||
@@ -1,63 +1,33 @@
|
||||
import type { FlattenedField, Payload, Where } from 'payload'
|
||||
import type { FlattenedField, Where } from 'payload'
|
||||
|
||||
import { QueryError } from 'payload'
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
type GetBuildQueryPluginArgs = {
|
||||
collectionSlug?: string
|
||||
versionsFields?: FlattenedField[]
|
||||
}
|
||||
|
||||
export type BuildQueryArgs = {
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
where: Where
|
||||
}
|
||||
|
||||
// This plugin asynchronously builds a list of Mongoose query constraints
|
||||
// which can then be used in subsequent Mongoose queries.
|
||||
export const getBuildQueryPlugin = ({
|
||||
export const buildQuery = async ({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
versionsFields,
|
||||
}: GetBuildQueryPluginArgs = {}) => {
|
||||
return function buildQueryPlugin(schema) {
|
||||
const modifiedSchema = schema
|
||||
async function buildQuery({
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
where,
|
||||
}: BuildQueryArgs): Promise<Record<string, unknown>> {
|
||||
let fields = versionsFields
|
||||
if (!fields) {
|
||||
if (globalSlug) {
|
||||
const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug)
|
||||
fields = globalConfig.flattenedFields
|
||||
}
|
||||
if (collectionSlug) {
|
||||
const collectionConfig = payload.collections[collectionSlug].config
|
||||
fields = collectionConfig.flattenedFields
|
||||
}
|
||||
}
|
||||
const errors = []
|
||||
}: {
|
||||
adapter: MongooseAdapter
|
||||
collectionSlug?: string
|
||||
fields: FlattenedField[]
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
where: Where
|
||||
}) => {
|
||||
const result = await parseParams({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
parentIsLocalized: false,
|
||||
payload,
|
||||
payload: adapter.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new QueryError(errors)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
modifiedSchema.statics.buildQuery = buildQuery
|
||||
}
|
||||
}
|
||||
|
||||
65
packages/db-mongodb/src/queries/getBuildQueryPlugin.ts
Normal file
65
packages/db-mongodb/src/queries/getBuildQueryPlugin.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { FlattenedField, Payload, Where } from 'payload'
|
||||
|
||||
import { QueryError } from 'payload'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
type GetBuildQueryPluginArgs = {
|
||||
collectionSlug?: string
|
||||
versionsFields?: FlattenedField[]
|
||||
}
|
||||
|
||||
export type BuildQueryArgs = {
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
where: Where
|
||||
}
|
||||
|
||||
// This plugin asynchronously builds a list of Mongoose query constraints
|
||||
// which can then be used in subsequent Mongoose queries.
|
||||
// Deprecated in favor of using simpler buildQuery directly
|
||||
export const getBuildQueryPlugin = ({
|
||||
collectionSlug,
|
||||
versionsFields,
|
||||
}: GetBuildQueryPluginArgs = {}) => {
|
||||
return function buildQueryPlugin(schema) {
|
||||
const modifiedSchema = schema
|
||||
async function schemaBuildQuery({
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
where,
|
||||
}: BuildQueryArgs): Promise<Record<string, unknown>> {
|
||||
let fields = versionsFields
|
||||
if (!fields) {
|
||||
if (globalSlug) {
|
||||
const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug)
|
||||
fields = globalConfig.flattenedFields
|
||||
}
|
||||
if (collectionSlug) {
|
||||
const collectionConfig = payload.collections[collectionSlug].config
|
||||
fields = collectionConfig.flattenedFields
|
||||
}
|
||||
}
|
||||
|
||||
const errors = []
|
||||
const result = await parseParams({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
parentIsLocalized: false,
|
||||
payload,
|
||||
where,
|
||||
})
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new QueryError(errors)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
modifiedSchema.statics.buildQuery = schemaBuildQuery
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,6 @@ export const sanitizeQueryValue = ({
|
||||
} => {
|
||||
let formattedValue = val
|
||||
let formattedOperator = operator
|
||||
|
||||
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
|
||||
const segments = path.split('.')
|
||||
segments.shift()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators }
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
@@ -41,15 +42,17 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
|
||||
const combinedWhere = combineQueries({ latest: { equals: true } }, where)
|
||||
|
||||
const versionQuery = await VersionModel.buildQuery({
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
const versionQuery = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: combinedWhere,
|
||||
})
|
||||
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
fields,
|
||||
select,
|
||||
})
|
||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
||||
|
||||
@@ -35,7 +35,7 @@ import type {
|
||||
UploadField,
|
||||
} from 'payload'
|
||||
|
||||
import type { BuildQueryArgs } from './queries/buildQuery.js'
|
||||
import type { BuildQueryArgs } from './queries/getBuildQueryPlugin.js'
|
||||
|
||||
export interface CollectionModel
|
||||
extends Model<any>,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
@@ -26,22 +27,23 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
|
||||
const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
|
||||
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
|
||||
|
||||
const flattenedFields = buildVersionGlobalFields(this.payload.config, currentGlobal, true)
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true),
|
||||
fields: flattenedFields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { UpdateOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
@@ -28,9 +29,11 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
@@ -19,25 +20,28 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
this.payload.collections[collection].config,
|
||||
)
|
||||
|
||||
const flattenedFields = buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
)
|
||||
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
fields: flattenedFields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: flattenedFields,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { PipelineStage } from 'mongoose'
|
||||
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
||||
import type {
|
||||
CollectionSlug,
|
||||
FlattenedField,
|
||||
JoinQuery,
|
||||
SanitizedCollectionConfig,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
import { fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { buildQuery } from '../queries/buildQuery.js'
|
||||
import { buildSortParam } from '../queries/buildSortParam.js'
|
||||
|
||||
type BuildJoinAggregationArgs = {
|
||||
@@ -33,11 +40,16 @@ export const buildJoinAggregation = async ({
|
||||
query,
|
||||
versions,
|
||||
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
|
||||
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
|
||||
if (
|
||||
(Object.keys(collectionConfig.joins).length === 0 &&
|
||||
collectionConfig.polymorphicJoins.length == 0) ||
|
||||
joins === false
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const joinConfig = adapter.payload.collections[collection].config.joins
|
||||
const polymorphicJoinsConfig = adapter.payload.collections[collection].config.polymorphicJoins
|
||||
const aggregate: PipelineStage[] = [
|
||||
{
|
||||
$sort: { createdAt: -1 },
|
||||
@@ -56,10 +68,7 @@ export const buildJoinAggregation = async ({
|
||||
})
|
||||
}
|
||||
|
||||
for (const slug of Object.keys(joinConfig)) {
|
||||
for (const join of joinConfig[slug]) {
|
||||
const joinModel = adapter.collections[join.field.collection]
|
||||
|
||||
for (const join of polymorphicJoinsConfig) {
|
||||
if (projection && !projection[join.joinPath]) {
|
||||
continue
|
||||
}
|
||||
@@ -75,6 +84,156 @@ export const buildJoinAggregation = async ({
|
||||
where: whereJoin,
|
||||
} = joins?.[join.joinPath] || {}
|
||||
|
||||
const aggregatedFields: FlattenedField[] = []
|
||||
for (const collectionSlug of join.field.collection) {
|
||||
for (const field of adapter.payload.collections[collectionSlug].config.flattenedFields) {
|
||||
if (!aggregatedFields.some((eachField) => eachField.name === field.name)) {
|
||||
aggregatedFields.push(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sort = buildSortParam({
|
||||
config: adapter.payload.config,
|
||||
fields: aggregatedFields,
|
||||
locale,
|
||||
sort: sortJoin,
|
||||
timestamps: true,
|
||||
})
|
||||
|
||||
const $match = await buildQuery({
|
||||
adapter,
|
||||
fields: aggregatedFields,
|
||||
locale,
|
||||
where: whereJoin,
|
||||
})
|
||||
|
||||
const sortProperty = Object.keys(sort)[0]
|
||||
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
|
||||
|
||||
const projectSort = sortProperty !== '_id' && sortProperty !== 'relationTo'
|
||||
|
||||
const aliases: string[] = []
|
||||
|
||||
const as = join.joinPath
|
||||
|
||||
for (const collectionSlug of join.field.collection) {
|
||||
const alias = `${as}.docs.${collectionSlug}`
|
||||
aliases.push(alias)
|
||||
|
||||
aggregate.push({
|
||||
$lookup: {
|
||||
as: alias,
|
||||
from: adapter.collections[collectionSlug].collection.name,
|
||||
let: {
|
||||
root_id_: '$_id',
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$addFields: {
|
||||
relationTo: {
|
||||
$literal: collectionSlug,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
$and: [
|
||||
{
|
||||
$expr: {
|
||||
$eq: [`$${join.field.on}`, '$$root_id_'],
|
||||
},
|
||||
},
|
||||
$match,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
[sortProperty]: sortDirection,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Unfortunately, we can't use $skip here because we can lose data, instead we do $slice then
|
||||
$limit: page ? page * limitJoin : limitJoin,
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
value: '$_id',
|
||||
...(projectSort && {
|
||||
[sortProperty]: 1,
|
||||
}),
|
||||
relationTo: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
[`${as}.docs`]: {
|
||||
$concatArrays: aliases.map((alias) => `$${alias}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
aggregate.push({
|
||||
$set: {
|
||||
[`${as}.docs`]: {
|
||||
$sortArray: {
|
||||
input: `$${as}.docs`,
|
||||
sortBy: {
|
||||
[sortProperty]: sortDirection,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const sliceValue = page ? [(page - 1) * limitJoin, limitJoin] : [limitJoin]
|
||||
|
||||
aggregate.push({
|
||||
$set: {
|
||||
[`${as}.docs`]: {
|
||||
$slice: [`$${as}.docs`, ...sliceValue],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
[`${as}.hasNextPage`]: {
|
||||
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (const slug of Object.keys(joinConfig)) {
|
||||
for (const join of joinConfig[slug]) {
|
||||
if (projection && !projection[join.joinPath]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (joins?.[join.joinPath] === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
const {
|
||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||
page,
|
||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||
where: whereJoin,
|
||||
} = joins?.[join.joinPath] || {}
|
||||
|
||||
if (Array.isArray(join.field.collection)) {
|
||||
throw new Error('Unreachable')
|
||||
}
|
||||
|
||||
const joinModel = adapter.collections[join.field.collection]
|
||||
|
||||
const sort = buildSortParam({
|
||||
config: adapter.payload.config,
|
||||
fields: adapter.payload.collections[slug].config.flattenedFields,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
||||
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
|
||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
@@ -10,11 +11,49 @@ import type { Result } from './buildFindManyArgs.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { getTableAlias } from '../queries/getTableAlias.js'
|
||||
import { operatorMap } from '../queries/operatorMap.js'
|
||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||
import { jsonAggBuildObject } from '../utilities/json.js'
|
||||
import { rawConstraint } from '../utilities/rawConstraint.js'
|
||||
import { chainMethods } from './chainMethods.js'
|
||||
|
||||
const flattenAllWherePaths = (where: Where, paths: string[]) => {
|
||||
for (const k in where) {
|
||||
if (['AND', 'OR'].includes(k.toUpperCase())) {
|
||||
if (Array.isArray(where[k])) {
|
||||
for (const whereField of where[k]) {
|
||||
flattenAllWherePaths(whereField, paths)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: explore how to support arrays/relationship querying.
|
||||
paths.push(k.split('.').join('_'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildSQLWhere = (where: Where, alias: string) => {
|
||||
for (const k in where) {
|
||||
if (['AND', 'OR'].includes(k.toUpperCase())) {
|
||||
if (Array.isArray(where[k])) {
|
||||
const op = 'AND' === k.toUpperCase() ? and : or
|
||||
const accumulated = []
|
||||
for (const whereField of where[k]) {
|
||||
accumulated.push(buildSQLWhere(whereField, alias))
|
||||
}
|
||||
return op(...accumulated)
|
||||
}
|
||||
} else {
|
||||
const payloadOperator = Object.keys(where[k])[0]
|
||||
const value = where[k][payloadOperator]
|
||||
|
||||
return operatorMap[payloadOperator](sql.raw(`"${alias}"."${k.split('.').join('_')}"`), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SQLSelect = SQLiteSelectBase<any, any, any, any>
|
||||
|
||||
type TraverseFieldArgs = {
|
||||
_locales: Result
|
||||
adapter: DrizzleAdapter
|
||||
@@ -359,6 +398,111 @@ export const traverseFields = ({
|
||||
limit += 1
|
||||
}
|
||||
|
||||
const columnName = `${path.replaceAll('.', '_')}${field.name}`
|
||||
|
||||
const db = adapter.drizzle as LibSQLDatabase
|
||||
|
||||
if (Array.isArray(field.collection)) {
|
||||
let currentQuery: null | SQLSelect = null
|
||||
const onPath = field.on.split('.').join('_')
|
||||
|
||||
if (Array.isArray(sort)) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
let sanitizedSort = sort
|
||||
|
||||
if (!sanitizedSort) {
|
||||
if (
|
||||
field.collection.some((collection) =>
|
||||
adapter.payload.collections[collection].config.fields.some(
|
||||
(f) => f.type === 'date' && f.name === 'createdAt',
|
||||
),
|
||||
)
|
||||
) {
|
||||
sanitizedSort = '-createdAt'
|
||||
} else {
|
||||
sanitizedSort = 'id'
|
||||
}
|
||||
}
|
||||
|
||||
const sortOrder = sanitizedSort.startsWith('-') ? desc : asc
|
||||
sanitizedSort = sanitizedSort.replace('-', '')
|
||||
|
||||
const sortPath = sanitizedSort.split('.').join('_')
|
||||
|
||||
const wherePaths: string[] = []
|
||||
|
||||
if (where) {
|
||||
flattenAllWherePaths(where, wherePaths)
|
||||
}
|
||||
|
||||
for (const collection of field.collection) {
|
||||
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(collection))
|
||||
|
||||
const table = adapter.tables[joinCollectionTableName]
|
||||
|
||||
const sortColumn = table[sortPath]
|
||||
|
||||
const selectFields = {
|
||||
id: adapter.tables[joinCollectionTableName].id,
|
||||
parent: sql`${adapter.tables[joinCollectionTableName][onPath]}`.as(onPath),
|
||||
relationTo: sql`${collection}`.as('relationTo'),
|
||||
sortPath: sql`${sortColumn ? sortColumn : null}`.as('sortPath'),
|
||||
}
|
||||
|
||||
// Select for WHERE and Fallback NULL
|
||||
for (const path of wherePaths) {
|
||||
if (adapter.tables[joinCollectionTableName][path]) {
|
||||
selectFields[path] = sql`${adapter.tables[joinCollectionTableName][path]}`.as(path)
|
||||
// Allow to filter by collectionSlug
|
||||
} else if (path !== 'relationTo') {
|
||||
selectFields[path] = sql`null`.as(path)
|
||||
}
|
||||
}
|
||||
|
||||
const query = db.select(selectFields).from(adapter.tables[joinCollectionTableName])
|
||||
if (currentQuery === null) {
|
||||
currentQuery = query as unknown as SQLSelect
|
||||
} else {
|
||||
currentQuery = currentQuery.unionAll(query) as SQLSelect
|
||||
}
|
||||
}
|
||||
|
||||
const subQueryAlias = `${columnName}_subquery`
|
||||
|
||||
let sqlWhere = eq(
|
||||
adapter.tables[currentTableName].id,
|
||||
sql.raw(`"${subQueryAlias}"."${onPath}"`),
|
||||
)
|
||||
|
||||
if (where && Object.keys(where).length > 0) {
|
||||
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias))
|
||||
}
|
||||
|
||||
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`)) as SQLSelect
|
||||
|
||||
if (page && limit !== 0) {
|
||||
const offset = (page - 1) * limit
|
||||
if (offset > 0) {
|
||||
currentQuery = currentQuery.offset(offset) as SQLSelect
|
||||
}
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
currentQuery = currentQuery.limit(limit) as SQLSelect
|
||||
}
|
||||
|
||||
currentArgs.extras[columnName] = sql`${db
|
||||
.select({
|
||||
id: jsonAggBuildObject(adapter, {
|
||||
id: sql.raw(`"${subQueryAlias}"."id"`),
|
||||
relationTo: sql.raw(`"${subQueryAlias}"."relationTo"`),
|
||||
}),
|
||||
})
|
||||
.from(sql`${currentQuery.as(subQueryAlias)}`)
|
||||
.where(sqlWhere)}`.as(columnName)
|
||||
} else {
|
||||
const fields = adapter.payload.collections[field.collection].config.flattenedFields
|
||||
|
||||
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
|
||||
@@ -413,9 +557,7 @@ export const traverseFields = ({
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
// Parent is never localized, as we're passing the `fields` of a **different** collection here. This means that the
|
||||
// parent localization "boundary" is crossed, and we're now in the context of the joined collection.
|
||||
parentIsLocalized: false,
|
||||
parentIsLocalized,
|
||||
selectLocale: true,
|
||||
sort,
|
||||
tableName: joinCollectionTableName,
|
||||
@@ -431,13 +573,6 @@ export const traverseFields = ({
|
||||
})
|
||||
})
|
||||
|
||||
if (limit !== 0) {
|
||||
chainedMethods.push({
|
||||
args: [limit],
|
||||
method: 'limit',
|
||||
})
|
||||
}
|
||||
|
||||
if (page && limit !== 0) {
|
||||
const offset = (page - 1) * limit - 1
|
||||
if (offset > 0) {
|
||||
@@ -448,6 +583,13 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (limit !== 0) {
|
||||
chainedMethods.push({
|
||||
args: [limit],
|
||||
method: 'limit',
|
||||
})
|
||||
}
|
||||
|
||||
const db = adapter.drizzle as LibSQLDatabase
|
||||
|
||||
for (let key in selectFields) {
|
||||
@@ -479,6 +621,7 @@ export const traverseFields = ({
|
||||
}),
|
||||
})
|
||||
.from(sql`${subQuery}`)}`.as(subQueryAlias)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -436,9 +436,14 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
} else {
|
||||
const hasNextPage = limit !== 0 && fieldData.length > limit
|
||||
fieldResult = {
|
||||
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(({ id }) => ({
|
||||
id,
|
||||
})),
|
||||
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(
|
||||
({ id, relationTo }) => {
|
||||
if (relationTo) {
|
||||
return { relationTo, value: id }
|
||||
}
|
||||
return { id }
|
||||
},
|
||||
),
|
||||
hasNextPage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,9 @@ export function buildObjectType({
|
||||
name: joinName,
|
||||
fields: {
|
||||
docs: {
|
||||
type: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
|
||||
},
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
},
|
||||
@@ -270,7 +272,9 @@ export function buildObjectType({
|
||||
type: GraphQLString,
|
||||
},
|
||||
where: {
|
||||
type: graphqlResult.collections[field.collection].graphQL.whereInputType,
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: graphqlResult.collections[field.collection].graphQL.whereInputType,
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
@@ -286,6 +290,10 @@ export function buildObjectType({
|
||||
[field.on]: { equals: parent._id ?? parent.id },
|
||||
})
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
throw new Error('GraphQL with array of join.field.collection is not implemented')
|
||||
}
|
||||
|
||||
return await req.payload.find({
|
||||
collection,
|
||||
depth: 0,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ImportMap } from '../../bin/generateImportMap/index.js'
|
||||
import type { SanitizedConfig } from '../../config/types.js'
|
||||
import type { PaginatedDocs } from '../../database/types.js'
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
|
||||
|
||||
export type DefaultServerFunctionArgs = {
|
||||
@@ -48,10 +49,15 @@ export type ListQuery = {
|
||||
}
|
||||
|
||||
export type BuildTableStateArgs = {
|
||||
collectionSlug: string
|
||||
collectionSlug: string | string[]
|
||||
columns?: { accessor: string; active: boolean }[]
|
||||
docs?: PaginatedDocs['docs']
|
||||
enableRowSelections?: boolean
|
||||
parent?: {
|
||||
collectionSlug: CollectionSlug
|
||||
id: number | string
|
||||
joinPath: string
|
||||
}
|
||||
query?: ListQuery
|
||||
renderRowTypes?: boolean
|
||||
req: PayloadRequest
|
||||
|
||||
@@ -17,7 +17,7 @@ import { createClientFields } from '../../fields/config/client.js'
|
||||
|
||||
export type ServerOnlyCollectionProperties = keyof Pick<
|
||||
SanitizedCollectionConfig,
|
||||
'access' | 'custom' | 'endpoints' | 'flattenedFields' | 'hooks' | 'joins'
|
||||
'access' | 'custom' | 'endpoints' | 'flattenedFields' | 'hooks' | 'joins' | 'polymorphicJoins'
|
||||
>
|
||||
|
||||
export type ServerOnlyCollectionAdminProperties = keyof Pick<
|
||||
@@ -68,6 +68,7 @@ const serverOnlyCollectionProperties: Partial<ServerOnlyCollectionProperties>[]
|
||||
'endpoints',
|
||||
'custom',
|
||||
'joins',
|
||||
'polymorphicJoins',
|
||||
'flattenedFields',
|
||||
// `upload`
|
||||
// `admin`
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// @ts-strict-ignore
|
||||
import type { LoginWithUsernameOptions } from '../../auth/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { CollectionConfig, SanitizedCollectionConfig, SanitizedJoins } from './types.js'
|
||||
import type {
|
||||
CollectionConfig,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedJoin,
|
||||
SanitizedJoins,
|
||||
} from './types.js'
|
||||
|
||||
import { authCollectionEndpoints } from '../../auth/endpoints/index.js'
|
||||
import { getBaseAuthFields } from '../../auth/getAuthFields.js'
|
||||
@@ -44,6 +49,7 @@ export const sanitizeCollection = async (
|
||||
const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
|
||||
|
||||
const joins: SanitizedJoins = {}
|
||||
const polymorphicJoins: SanitizedJoin[] = []
|
||||
sanitized.fields = await sanitizeFields({
|
||||
collectionConfig: sanitized,
|
||||
config,
|
||||
@@ -51,6 +57,7 @@ export const sanitizeCollection = async (
|
||||
joinPath: '',
|
||||
joins,
|
||||
parentIsLocalized: false,
|
||||
polymorphicJoins,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
@@ -234,6 +241,7 @@ export const sanitizeCollection = async (
|
||||
const sanitizedConfig = sanitized as SanitizedCollectionConfig
|
||||
|
||||
sanitizedConfig.joins = joins
|
||||
sanitizedConfig.polymorphicJoins = polymorphicJoins
|
||||
|
||||
sanitizedConfig.flattenedFields = flattenAllFields({ fields: sanitizedConfig.fields })
|
||||
|
||||
|
||||
@@ -548,6 +548,12 @@ export interface SanitizedCollectionConfig
|
||||
* Object of collections to join 'Join Fields object keyed by collection
|
||||
*/
|
||||
joins: SanitizedJoins
|
||||
|
||||
/**
|
||||
* List of all polymorphic join fields
|
||||
*/
|
||||
polymorphicJoins: SanitizedJoin[]
|
||||
|
||||
slug: CollectionSlug
|
||||
upload: SanitizedUploadConfig
|
||||
versions: SanitizedCollectionVersions
|
||||
|
||||
@@ -107,6 +107,18 @@ export function getLocalizedPaths({
|
||||
return paths
|
||||
}
|
||||
|
||||
if (currentPath === 'relationTo') {
|
||||
lastIncompletePath.path = currentPath
|
||||
lastIncompletePath.complete = true
|
||||
lastIncompletePath.field = {
|
||||
name: 'relationTo',
|
||||
type: 'select',
|
||||
options: Object.keys(payload.collections),
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
if (!matchedField && currentPath === 'id' && i === pathSegments.length - 1) {
|
||||
lastIncompletePath.path = currentPath
|
||||
const idField: Field = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
||||
import type { SanitizedCollectionConfig, SanitizedJoin } from '../collections/config/types.js'
|
||||
import type { JoinQuery, PayloadRequest } from '../types/index.js'
|
||||
|
||||
import executeAccess from '../auth/executeAccess.js'
|
||||
@@ -14,6 +14,70 @@ type Args = {
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
const sanitizeJoinFieldQuery = async ({
|
||||
collectionSlug,
|
||||
errors,
|
||||
join,
|
||||
joinsQuery,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
}: {
|
||||
collectionSlug: string
|
||||
errors: { path: string }[]
|
||||
join: SanitizedJoin
|
||||
joinsQuery: JoinQuery
|
||||
overrideAccess: boolean
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
}) => {
|
||||
const { joinPath } = join
|
||||
|
||||
if (joinsQuery[joinPath] === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const joinCollectionConfig = req.payload.collections[collectionSlug].config
|
||||
|
||||
const accessResult = !overrideAccess
|
||||
? await executeAccess({ disableErrors: true, req }, joinCollectionConfig.access.read)
|
||||
: true
|
||||
|
||||
if (accessResult === false) {
|
||||
joinsQuery[joinPath] = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!joinsQuery[joinPath]) {
|
||||
joinsQuery[joinPath] = {}
|
||||
}
|
||||
|
||||
const joinQuery = joinsQuery[joinPath]
|
||||
|
||||
if (!joinQuery.where) {
|
||||
joinQuery.where = {}
|
||||
}
|
||||
|
||||
if (join.field.where) {
|
||||
joinQuery.where = combineQueries(joinQuery.where, join.field.where)
|
||||
}
|
||||
|
||||
promises.push(
|
||||
validateQueryPaths({
|
||||
collectionConfig: joinCollectionConfig,
|
||||
errors,
|
||||
overrideAccess,
|
||||
req,
|
||||
// incoming where input, but we shouldn't validate generated from the access control.
|
||||
where: joinQuery.where,
|
||||
}),
|
||||
)
|
||||
|
||||
if (typeof accessResult === 'object') {
|
||||
joinQuery.where = combineQueries(joinQuery.where, accessResult)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* * Validates `where` for each join
|
||||
* * Combines the access result for joined collection
|
||||
@@ -37,51 +101,31 @@ export const sanitizeJoinQuery = async ({
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
for (const collectionSlug in collectionConfig.joins) {
|
||||
for (const { field, joinPath } of collectionConfig.joins[collectionSlug]) {
|
||||
if (joinsQuery[joinPath] === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
const joinCollectionConfig = req.payload.collections[collectionSlug].config
|
||||
|
||||
const accessResult = !overrideAccess
|
||||
? await executeAccess({ disableErrors: true, req }, joinCollectionConfig.access.read)
|
||||
: true
|
||||
|
||||
if (accessResult === false) {
|
||||
joinsQuery[joinPath] = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (!joinsQuery[joinPath]) {
|
||||
joinsQuery[joinPath] = {}
|
||||
}
|
||||
|
||||
const joinQuery = joinsQuery[joinPath]
|
||||
|
||||
if (!joinQuery.where) {
|
||||
joinQuery.where = {}
|
||||
}
|
||||
|
||||
if (field.where) {
|
||||
joinQuery.where = combineQueries(joinQuery.where, field.where)
|
||||
}
|
||||
|
||||
promises.push(
|
||||
validateQueryPaths({
|
||||
collectionConfig: joinCollectionConfig,
|
||||
for (const join of collectionConfig.joins[collectionSlug]) {
|
||||
await sanitizeJoinFieldQuery({
|
||||
collectionSlug,
|
||||
errors,
|
||||
join,
|
||||
joinsQuery,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
// incoming where input, but we shouldn't validate generated from the access control.
|
||||
where: joinQuery.where,
|
||||
}),
|
||||
)
|
||||
|
||||
if (typeof accessResult === 'object') {
|
||||
joinQuery.where = combineQueries(joinQuery.where, accessResult)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const join of collectionConfig.polymorphicJoins) {
|
||||
for (const collectionSlug of join.field.collection) {
|
||||
await sanitizeJoinFieldQuery({
|
||||
collectionSlug,
|
||||
errors,
|
||||
join,
|
||||
joinsQuery,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
@@ -311,7 +311,7 @@ export const createClientField = ({
|
||||
const field = clientField as JoinFieldClient
|
||||
|
||||
field.targetField = {
|
||||
relationTo: field.targetField.relationTo,
|
||||
relationTo: field.targetField?.relationTo,
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// @ts-strict-ignore
|
||||
import { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
import type { CollectionConfig, SanitizedJoins } from '../../collections/config/types.js'
|
||||
import type {
|
||||
CollectionConfig,
|
||||
SanitizedJoin,
|
||||
SanitizedJoins,
|
||||
} from '../../collections/config/types.js'
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type { Field } from './types.js'
|
||||
|
||||
@@ -33,6 +37,7 @@ type Args = {
|
||||
*/
|
||||
joins?: SanitizedJoins
|
||||
parentIsLocalized: boolean
|
||||
polymorphicJoins?: SanitizedJoin[]
|
||||
|
||||
/**
|
||||
* If true, a richText field will require an editor property to be set, as the sanitizeFields function will not add it from the payload config if not present.
|
||||
@@ -59,6 +64,7 @@ export const sanitizeFields = async ({
|
||||
joinPath = '',
|
||||
joins,
|
||||
parentIsLocalized,
|
||||
polymorphicJoins,
|
||||
requireFieldLevelRichTextEditor = false,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
@@ -104,7 +110,7 @@ export const sanitizeFields = async ({
|
||||
}
|
||||
|
||||
if (field.type === 'join') {
|
||||
sanitizeJoinField({ config, field, joinPath, joins, parentIsLocalized })
|
||||
sanitizeJoinField({ config, field, joinPath, joins, parentIsLocalized, polymorphicJoins })
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
@@ -265,6 +271,7 @@ export const sanitizeFields = async ({
|
||||
: joinPath,
|
||||
joins,
|
||||
parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
|
||||
polymorphicJoins,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
@@ -285,6 +292,7 @@ export const sanitizeFields = async ({
|
||||
joinPath: tabHasName(tab) ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
|
||||
joins,
|
||||
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
||||
polymorphicJoins,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
|
||||
@@ -18,12 +18,16 @@ export const sanitizeJoinField = ({
|
||||
joinPath,
|
||||
joins,
|
||||
parentIsLocalized,
|
||||
polymorphicJoins,
|
||||
validateOnly,
|
||||
}: {
|
||||
config: Config
|
||||
field: FlattenedJoinField | JoinField
|
||||
joinPath?: string
|
||||
joins?: SanitizedJoins
|
||||
parentIsLocalized: boolean
|
||||
polymorphicJoins?: SanitizedJoin[]
|
||||
validateOnly?: boolean
|
||||
}) => {
|
||||
// the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field
|
||||
if (typeof joins === 'undefined') {
|
||||
@@ -38,6 +42,32 @@ export const sanitizeJoinField = ({
|
||||
parentIsLocalized,
|
||||
targetField: undefined,
|
||||
}
|
||||
|
||||
if (Array.isArray(field.collection)) {
|
||||
for (const collection of field.collection) {
|
||||
const sanitizedField = {
|
||||
...field,
|
||||
collection,
|
||||
} as FlattenedJoinField
|
||||
|
||||
sanitizeJoinField({
|
||||
config,
|
||||
field: sanitizedField,
|
||||
joinPath,
|
||||
joins,
|
||||
parentIsLocalized,
|
||||
polymorphicJoins,
|
||||
validateOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(polymorphicJoins)) {
|
||||
polymorphicJoins.push(join)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const joinCollection = config.collections.find(
|
||||
(collection) => collection.slug === field.collection,
|
||||
)
|
||||
@@ -109,6 +139,10 @@ export const sanitizeJoinField = ({
|
||||
throw new InvalidFieldJoin(join.field)
|
||||
}
|
||||
|
||||
if (validateOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
join.targetField = joinRelationship
|
||||
|
||||
// override the join field localized property to use whatever the relationship field has
|
||||
|
||||
@@ -1478,7 +1478,7 @@ export type JoinField = {
|
||||
/**
|
||||
* The slug of the collection to relate with.
|
||||
*/
|
||||
collection: CollectionSlug
|
||||
collection: CollectionSlug | CollectionSlug[]
|
||||
defaultLimit?: number
|
||||
defaultSort?: Sort
|
||||
defaultValue?: never
|
||||
@@ -1504,6 +1504,7 @@ export type JoinField = {
|
||||
* A string for the field in the collection being joined to.
|
||||
*/
|
||||
on: string
|
||||
sanitizedMany?: JoinField[]
|
||||
type: 'join'
|
||||
validate?: never
|
||||
where?: Where
|
||||
|
||||
@@ -22,6 +22,7 @@ type PopulateArgs = {
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
// TODO: this function is mess, refactor logic
|
||||
const populate = async ({
|
||||
currentDepth,
|
||||
data,
|
||||
@@ -41,14 +42,24 @@ const populate = async ({
|
||||
const dataToUpdate = dataReference
|
||||
let relation
|
||||
if (field.type === 'join') {
|
||||
relation = field.collection
|
||||
relation = Array.isArray(field.collection) ? data.relationTo : field.collection
|
||||
} else {
|
||||
relation = Array.isArray(field.relationTo) ? (data.relationTo as string) : field.relationTo
|
||||
}
|
||||
|
||||
const relatedCollection = req.payload.collections[relation]
|
||||
|
||||
if (relatedCollection) {
|
||||
let id = field.type !== 'join' && Array.isArray(field.relationTo) ? data.value : data
|
||||
let id: unknown
|
||||
|
||||
if (field.type === 'join' && Array.isArray(field.collection)) {
|
||||
id = data.value
|
||||
} else if (field.type !== 'join' && Array.isArray(field.relationTo)) {
|
||||
id = data.value
|
||||
} else {
|
||||
id = data
|
||||
}
|
||||
|
||||
let relationshipValue
|
||||
const shouldPopulate = depth && currentDepth <= depth
|
||||
|
||||
@@ -89,12 +100,20 @@ const populate = async ({
|
||||
if (typeof index === 'number' && typeof key === 'string') {
|
||||
if (field.type !== 'join' && Array.isArray(field.relationTo)) {
|
||||
dataToUpdate[field.name][key][index].value = relationshipValue
|
||||
} else {
|
||||
if (field.type === 'join' && Array.isArray(field.collection)) {
|
||||
dataToUpdate[field.name][key][index].value = relationshipValue
|
||||
} else {
|
||||
dataToUpdate[field.name][key][index] = relationshipValue
|
||||
}
|
||||
}
|
||||
} else if (typeof index === 'number' || typeof key === 'string') {
|
||||
if (field.type === 'join') {
|
||||
if (!Array.isArray(field.collection)) {
|
||||
dataToUpdate[field.name].docs[index ?? key] = relationshipValue
|
||||
} else {
|
||||
dataToUpdate[field.name].docs[index ?? key].value = relationshipValue
|
||||
}
|
||||
} else if (Array.isArray(field.relationTo)) {
|
||||
dataToUpdate[field.name][index ?? key].value = relationshipValue
|
||||
} else {
|
||||
@@ -102,10 +121,14 @@ const populate = async ({
|
||||
}
|
||||
} else if (field.type !== 'join' && Array.isArray(field.relationTo)) {
|
||||
dataToUpdate[field.name].value = relationshipValue
|
||||
} else {
|
||||
if (field.type === 'join' && Array.isArray(field.collection)) {
|
||||
dataToUpdate[field.name].value = relationshipValue
|
||||
} else {
|
||||
dataToUpdate[field.name] = relationshipValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PromiseArgs = {
|
||||
@@ -185,7 +208,10 @@ export const relationshipPopulationPromise = async ({
|
||||
if (relatedDoc) {
|
||||
await populate({
|
||||
currentDepth,
|
||||
data: relatedDoc?.id ? relatedDoc.id : relatedDoc,
|
||||
data:
|
||||
!(field.type === 'join' && Array.isArray(field.collection)) && relatedDoc?.id
|
||||
? relatedDoc.id
|
||||
: relatedDoc,
|
||||
dataReference: resultingDoc,
|
||||
depth: populateDepth,
|
||||
draft,
|
||||
|
||||
@@ -1246,6 +1246,7 @@ export type {
|
||||
FlattenedBlocksField,
|
||||
FlattenedField,
|
||||
FlattenedGroupField,
|
||||
FlattenedJoinField,
|
||||
FlattenedTabAsField,
|
||||
GroupField,
|
||||
GroupFieldClient,
|
||||
|
||||
@@ -88,7 +88,7 @@ function generateEntitySelectSchemas(
|
||||
|
||||
function generateCollectionJoinsSchemas(collections: SanitizedCollectionConfig[]): JSONSchema4 {
|
||||
const properties = [...collections].reduce<Record<string, JSONSchema4>>(
|
||||
(acc, { slug, joins }) => {
|
||||
(acc, { slug, joins, polymorphicJoins }) => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -106,6 +106,14 @@ function generateCollectionJoinsSchemas(collections: SanitizedCollectionConfig[]
|
||||
}
|
||||
}
|
||||
|
||||
for (const join of polymorphicJoins) {
|
||||
schema.properties[join.joinPath] = {
|
||||
type: 'string',
|
||||
enum: join.field.collection,
|
||||
}
|
||||
schema.required.push(join.joinPath)
|
||||
}
|
||||
|
||||
if (Object.keys(schema.properties).length > 0) {
|
||||
acc[slug] = schema
|
||||
}
|
||||
@@ -387,14 +395,33 @@ export function fieldsToJSONSchema(
|
||||
}
|
||||
|
||||
case 'join': {
|
||||
fieldSchema = {
|
||||
...baseFieldSchema,
|
||||
type: withNullableJSONSchemaType('object', false),
|
||||
let items: JSONSchema4
|
||||
|
||||
if (Array.isArray(field.collection)) {
|
||||
items = {
|
||||
oneOf: field.collection.map((collection) => ({
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
docs: {
|
||||
type: withNullableJSONSchemaType('array', false),
|
||||
items: {
|
||||
relationTo: {
|
||||
const: collection,
|
||||
},
|
||||
value: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[collection],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${collection}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['collectionSlug', 'value'],
|
||||
})),
|
||||
}
|
||||
} else {
|
||||
items = {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[field.collection],
|
||||
@@ -403,7 +430,17 @@ export function fieldsToJSONSchema(
|
||||
$ref: `#/definitions/${field.collection}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fieldSchema = {
|
||||
...baseFieldSchema,
|
||||
type: withNullableJSONSchemaType('object', false),
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
docs: {
|
||||
type: withNullableJSONSchemaType('array', false),
|
||||
items,
|
||||
},
|
||||
hasNextPage: { type: withNullableJSONSchemaType('boolean', false) },
|
||||
},
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
padding-bottom: var(--base);
|
||||
}
|
||||
|
||||
&__add-new-polymorphic .btn__label {
|
||||
display: flex;
|
||||
text-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table {
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
'use client'
|
||||
import type { Column, JoinFieldClient, ListQuery, PaginatedDocs, Where } from 'payload'
|
||||
import type {
|
||||
CollectionSlug,
|
||||
Column,
|
||||
JoinFieldClient,
|
||||
ListQuery,
|
||||
PaginatedDocs,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
||||
@@ -10,6 +17,7 @@ import { Button } from '../../elements/Button/index.js'
|
||||
import { Pill } from '../../elements/Pill/index.js'
|
||||
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
|
||||
import { ChevronIcon } from '../../icons/Chevron/index.js'
|
||||
import { PlusIcon } from '../../icons/Plus/index.js'
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { ListQueryProvider } from '../../providers/ListQuery/index.js'
|
||||
@@ -17,9 +25,10 @@ import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { hoistQueryParamsToAnd } from '../../utilities/mergeListSearchAndWhere.js'
|
||||
import { AnimateHeight } from '../AnimateHeight/index.js'
|
||||
import { ColumnSelector } from '../ColumnSelector/index.js'
|
||||
import './index.scss'
|
||||
import { ColumnSelector } from '../ColumnSelector/index.js'
|
||||
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
|
||||
import { Popup, PopupList } from '../Popup/index.js'
|
||||
import { RelationshipProvider } from '../Table/RelationshipProvider/index.js'
|
||||
import { TableColumnsProvider } from '../TableColumns/index.js'
|
||||
import { DrawerLink } from './cells/DrawerLink/index.js'
|
||||
@@ -37,7 +46,12 @@ type RelationshipTableComponentProps = {
|
||||
readonly initialData?: PaginatedDocs
|
||||
readonly initialDrawerData?: DocumentDrawerProps['initialData']
|
||||
readonly Label?: React.ReactNode
|
||||
readonly relationTo: string
|
||||
readonly parent?: {
|
||||
collectionSlug: CollectionSlug
|
||||
id: number | string
|
||||
joinPath: string
|
||||
}
|
||||
readonly relationTo: string | string[]
|
||||
}
|
||||
|
||||
export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (props) => {
|
||||
@@ -51,10 +65,11 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
initialData: initialDataFromProps,
|
||||
initialDrawerData,
|
||||
Label,
|
||||
parent,
|
||||
relationTo,
|
||||
} = props
|
||||
const [Table, setTable] = useState<React.ReactNode>(null)
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { config, getEntityConfig } = useConfig()
|
||||
|
||||
const { permissions } = useAuth()
|
||||
|
||||
@@ -86,6 +101,9 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
|
||||
const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug: relationTo }))
|
||||
|
||||
const [selectedCollection, setSelectedCollection] = useState(
|
||||
Array.isArray(relationTo) ? undefined : relationTo,
|
||||
)
|
||||
const [isLoadingTable, setIsLoadingTable] = useState(!disableTable)
|
||||
const [data, setData] = useState<PaginatedDocs>(initialData)
|
||||
const [columnState, setColumnState] = useState<Column[]>()
|
||||
@@ -95,8 +113,8 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
const renderTable = useCallback(
|
||||
async (docs?: PaginatedDocs['docs']) => {
|
||||
const newQuery: ListQuery = {
|
||||
limit: String(field.defaultLimit || collectionConfig.admin.pagination.defaultLimit),
|
||||
sort: field.defaultSort || collectionConfig.defaultSort,
|
||||
limit: String(field?.defaultLimit || collectionConfig?.admin?.pagination?.defaultLimit),
|
||||
sort: field.defaultSort || collectionConfig?.defaultSort,
|
||||
...(query || {}),
|
||||
where: { ...(query?.where || {}) },
|
||||
}
|
||||
@@ -122,6 +140,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
columns: defaultColumns,
|
||||
docs,
|
||||
enableRowSelections: false,
|
||||
parent,
|
||||
query: newQuery,
|
||||
renderRowTypes: true,
|
||||
tableAppearance: 'condensed',
|
||||
@@ -136,12 +155,13 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
field.defaultLimit,
|
||||
field.defaultSort,
|
||||
field.admin.defaultColumns,
|
||||
collectionConfig.admin.pagination.defaultLimit,
|
||||
collectionConfig.defaultSort,
|
||||
collectionConfig?.admin?.pagination?.defaultLimit,
|
||||
collectionConfig?.defaultSort,
|
||||
query,
|
||||
filterOptions,
|
||||
getTableState,
|
||||
relationTo,
|
||||
parent,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -155,8 +175,9 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
handleTableRender(query, disableTable)
|
||||
}, [query, disableTable])
|
||||
|
||||
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer, openDrawer }] = useDocumentDrawer({
|
||||
collectionSlug: relationTo,
|
||||
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer, isDrawerOpen, openDrawer }] =
|
||||
useDocumentDrawer({
|
||||
collectionSlug: selectedCollection,
|
||||
})
|
||||
|
||||
const onDrawerSave = useCallback<DocumentDrawerProps['onSave']>(
|
||||
@@ -174,12 +195,13 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
|
||||
void renderTable(withNewOrUpdatedDoc)
|
||||
},
|
||||
[data.docs, renderTable],
|
||||
[data?.docs, renderTable],
|
||||
)
|
||||
|
||||
const onDrawerCreate = useCallback<DocumentDrawerProps['onSave']>(
|
||||
(args) => {
|
||||
closeDrawer()
|
||||
|
||||
void onDrawerSave(args)
|
||||
},
|
||||
[closeDrawer, onDrawerSave],
|
||||
@@ -190,23 +212,80 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
const newDocs = data.docs.filter((doc) => doc.id !== args.id)
|
||||
void renderTable(newDocs)
|
||||
},
|
||||
[data.docs, renderTable],
|
||||
[data?.docs, renderTable],
|
||||
)
|
||||
|
||||
const preferenceKey = `${relationTo}-list`
|
||||
const preferenceKey = `${Array.isArray(relationTo) ? `${parent.collectionSlug}-${parent.joinPath}` : relationTo}-list`
|
||||
|
||||
const canCreate = allowCreate !== false && permissions?.collections?.[relationTo]?.create
|
||||
const canCreate =
|
||||
allowCreate !== false &&
|
||||
permissions?.collections?.[Array.isArray(relationTo) ? relationTo[0] : relationTo]?.create
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(relationTo) && selectedCollection) {
|
||||
openDrawer()
|
||||
}
|
||||
}, [selectedCollection, openDrawer, relationTo])
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(relationTo) && !isDrawerOpen && selectedCollection) {
|
||||
setSelectedCollection(undefined)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDrawerOpen])
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
{Label}
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{canCreate && (
|
||||
{!Array.isArray(relationTo) && canCreate && (
|
||||
<DocumentDrawerToggler className={`${baseClass}__add-new`}>
|
||||
{i18n.t('fields:addNew')}
|
||||
</DocumentDrawerToggler>
|
||||
)}
|
||||
|
||||
{Array.isArray(relationTo) && (
|
||||
<Fragment>
|
||||
<Popup
|
||||
button={
|
||||
<Button buttonStyle="none" className={`${baseClass}__add-new-polymorphic`}>
|
||||
{i18n.t('fields:addNew')}
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
}
|
||||
buttonType="custom"
|
||||
horizontalAlign="center"
|
||||
render={({ close: closePopup }) => (
|
||||
<PopupList.ButtonGroup>
|
||||
{relationTo.map((relatedCollection) => {
|
||||
if (permissions.collections[relatedCollection].create) {
|
||||
return (
|
||||
<PopupList.Button
|
||||
className={`${baseClass}__relation-button--${relatedCollection}`}
|
||||
key={relatedCollection}
|
||||
onClick={() => {
|
||||
closePopup()
|
||||
setSelectedCollection(relatedCollection)
|
||||
}}
|
||||
>
|
||||
{getTranslation(
|
||||
config.collections.find((each) => each.slug === relatedCollection)
|
||||
.labels.singular,
|
||||
i18n,
|
||||
)}
|
||||
</PopupList.Button>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</PopupList.ButtonGroup>
|
||||
)}
|
||||
size="medium"
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
<Pill
|
||||
aria-controls={`${baseClass}-columns`}
|
||||
aria-expanded={openColumnSelector}
|
||||
@@ -226,11 +305,13 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
<p>{t('general:loading')}</p>
|
||||
) : (
|
||||
<Fragment>
|
||||
{data.docs && data.docs.length === 0 && (
|
||||
{data?.docs && data.docs.length === 0 && (
|
||||
<div className={`${baseClass}__no-results`}>
|
||||
<p>
|
||||
{i18n.t('general:noResults', {
|
||||
label: getTranslation(collectionConfig?.labels?.plural, i18n),
|
||||
label: Array.isArray(relationTo)
|
||||
? i18n.t('general:documents')
|
||||
: getTranslation(collectionConfig?.labels?.plural, i18n),
|
||||
})}
|
||||
</p>
|
||||
{canCreate && (
|
||||
@@ -242,7 +323,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{data.docs && data.docs.length > 0 && (
|
||||
{data?.docs && data.docs.length > 0 && (
|
||||
<RelationshipProvider>
|
||||
<ListQueryProvider
|
||||
data={data}
|
||||
@@ -253,7 +334,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
onQueryChange={setQuery}
|
||||
>
|
||||
<TableColumnsProvider
|
||||
collectionSlug={relationTo}
|
||||
collectionSlug={Array.isArray(relationTo) ? relationTo[0] : relationTo}
|
||||
columnState={columnState}
|
||||
docs={data.docs}
|
||||
LinkedCellOverride={
|
||||
@@ -273,7 +354,9 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
||||
id={`${baseClass}-columns`}
|
||||
>
|
||||
<div className={`${baseClass}__columns-inner`}>
|
||||
{collectionConfig && (
|
||||
<ColumnSelector collectionSlug={collectionConfig.slug} />
|
||||
)}
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
{Table}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
// Dirty copy of buildColumnState.tsx with some changes to not break things
|
||||
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
ClientField,
|
||||
Column,
|
||||
DefaultCellComponentProps,
|
||||
DefaultServerCellComponentProps,
|
||||
Field,
|
||||
ListPreferences,
|
||||
PaginatedDocs,
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
StaticLabel,
|
||||
} from 'payload'
|
||||
|
||||
import { MissingEditorProp } from 'payload'
|
||||
import {
|
||||
fieldIsHiddenOrDisabled,
|
||||
fieldIsID,
|
||||
fieldIsPresentationalOnly,
|
||||
flattenTopLevelFields,
|
||||
} from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import type { SortColumnProps } from '../SortColumn/index.js'
|
||||
|
||||
import {
|
||||
RenderCustomComponent,
|
||||
RenderDefaultCell,
|
||||
SortColumn,
|
||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||
} from '../../exports/client/index.js'
|
||||
import { RenderServerComponent } from '../RenderServerComponent/index.js'
|
||||
import { filterFields } from './filterFields.js'
|
||||
|
||||
type Args = {
|
||||
beforeRows?: Column[]
|
||||
columnPreferences: ListPreferences['columns']
|
||||
columns?: ListPreferences['columns']
|
||||
customCellProps: DefaultCellComponentProps['customCellProps']
|
||||
docs: PaginatedDocs['docs']
|
||||
enableRowSelections: boolean
|
||||
enableRowTypes?: boolean
|
||||
fields: ClientField[]
|
||||
i18n: I18nClient
|
||||
payload: Payload
|
||||
sortColumnProps?: Partial<SortColumnProps>
|
||||
useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
|
||||
}
|
||||
|
||||
export const buildPolymorphicColumnState = (args: Args): Column[] => {
|
||||
const {
|
||||
beforeRows,
|
||||
columnPreferences,
|
||||
columns,
|
||||
customCellProps,
|
||||
docs,
|
||||
enableRowSelections,
|
||||
fields,
|
||||
i18n,
|
||||
payload,
|
||||
sortColumnProps,
|
||||
useAsTitle,
|
||||
} = args
|
||||
|
||||
// clientFields contains the fake `id` column
|
||||
let sortedFieldMap = flattenTopLevelFields(filterFields(fields), true) as ClientField[]
|
||||
|
||||
let _sortedFieldMap = flattenTopLevelFields(filterFields(fields), true) as Field[] // TODO: think of a way to avoid this additional flatten
|
||||
|
||||
// place the `ID` field first, if it exists
|
||||
// do the same for the `useAsTitle` field with precedence over the `ID` field
|
||||
// then sort the rest of the fields based on the `defaultColumns` or `columnPreferences`
|
||||
const idFieldIndex = sortedFieldMap?.findIndex((field) => fieldIsID(field))
|
||||
|
||||
if (idFieldIndex > -1) {
|
||||
const idField = sortedFieldMap.splice(idFieldIndex, 1)[0]
|
||||
sortedFieldMap.unshift(idField)
|
||||
}
|
||||
|
||||
const useAsTitleFieldIndex = useAsTitle
|
||||
? sortedFieldMap.findIndex((field) => 'name' in field && field.name === useAsTitle)
|
||||
: -1
|
||||
|
||||
if (useAsTitleFieldIndex > -1) {
|
||||
const useAsTitleField = sortedFieldMap.splice(useAsTitleFieldIndex, 1)[0]
|
||||
sortedFieldMap.unshift(useAsTitleField)
|
||||
}
|
||||
|
||||
const sortTo = columnPreferences || columns
|
||||
|
||||
const sortFieldMap = (fieldMap, sortTo) =>
|
||||
fieldMap?.sort((a, b) => {
|
||||
const aIndex = sortTo.findIndex((column) => 'name' in a && column.accessor === a.name)
|
||||
const bIndex = sortTo.findIndex((column) => 'name' in b && column.accessor === b.name)
|
||||
|
||||
if (aIndex === -1 && bIndex === -1) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (aIndex === -1) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (bIndex === -1) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return aIndex - bIndex
|
||||
})
|
||||
|
||||
if (sortTo) {
|
||||
// sort the fields to the order of `defaultColumns` or `columnPreferences`
|
||||
sortedFieldMap = sortFieldMap(sortedFieldMap, sortTo)
|
||||
_sortedFieldMap = sortFieldMap(_sortedFieldMap, sortTo) // TODO: think of a way to avoid this additional sort
|
||||
}
|
||||
|
||||
const activeColumnsIndices = []
|
||||
|
||||
const sorted: Column[] = sortedFieldMap?.reduce((acc, field, index) => {
|
||||
if (fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const _field = _sortedFieldMap.find(
|
||||
(f) => 'name' in field && 'name' in f && f.name === field.name,
|
||||
)
|
||||
|
||||
const columnPreference = columnPreferences?.find(
|
||||
(preference) => field && 'name' in field && preference.accessor === field.name,
|
||||
)
|
||||
|
||||
let active = false
|
||||
|
||||
if (columnPreference) {
|
||||
active = columnPreference.active
|
||||
} else if (columns && Array.isArray(columns) && columns.length > 0) {
|
||||
active = columns.find(
|
||||
(column) => field && 'name' in field && column.accessor === field.name,
|
||||
)?.active
|
||||
} else if (activeColumnsIndices.length < 4) {
|
||||
active = true
|
||||
}
|
||||
|
||||
if (active && !activeColumnsIndices.includes(index)) {
|
||||
activeColumnsIndices.push(index)
|
||||
}
|
||||
|
||||
// const CustomLabelToRender =
|
||||
// _field &&
|
||||
// 'admin' in _field &&
|
||||
// 'components' in _field.admin &&
|
||||
// 'Label' in _field.admin.components &&
|
||||
// _field.admin.components.Label !== undefined // let it return `null`
|
||||
// ? _field.admin.components.Label
|
||||
// : undefined
|
||||
|
||||
// // TODO: customComponent will be optional in v4
|
||||
// const clientProps: Omit<ClientComponentProps, 'customComponents'> = {
|
||||
// field,
|
||||
// }
|
||||
|
||||
// const customLabelServerProps: Pick<
|
||||
// ServerComponentProps,
|
||||
// 'clientField' | 'collectionSlug' | 'field' | 'i18n' | 'payload'
|
||||
// > = {
|
||||
// clientField: field,
|
||||
// collectionSlug: collectionConfig.slug,
|
||||
// field: _field,
|
||||
// i18n,
|
||||
// payload,
|
||||
// }
|
||||
|
||||
const CustomLabel = undefined
|
||||
|
||||
const fieldAffectsDataSubFields =
|
||||
field &&
|
||||
field.type &&
|
||||
(field.type === 'array' || field.type === 'group' || field.type === 'blocks')
|
||||
|
||||
const Heading = (
|
||||
<SortColumn
|
||||
disable={fieldAffectsDataSubFields || fieldIsPresentationalOnly(field) || undefined}
|
||||
Label={CustomLabel}
|
||||
label={field && 'label' in field ? (field.label as StaticLabel) : undefined}
|
||||
name={'name' in field ? field.name : undefined}
|
||||
{...(sortColumnProps || {})}
|
||||
/>
|
||||
)
|
||||
|
||||
const column: Column = {
|
||||
accessor: 'name' in field ? field.name : undefined,
|
||||
active,
|
||||
CustomLabel,
|
||||
field,
|
||||
Heading,
|
||||
renderedCells: active
|
||||
? docs.map((doc, i) => {
|
||||
const isLinkedColumn = index === activeColumnsIndices[0]
|
||||
|
||||
const collectionSlug = doc.relationTo
|
||||
doc = doc.value
|
||||
|
||||
const baseCellClientProps: DefaultCellComponentProps = {
|
||||
cellData: undefined,
|
||||
collectionSlug,
|
||||
customCellProps,
|
||||
field,
|
||||
rowData: undefined,
|
||||
}
|
||||
|
||||
const cellClientProps: DefaultCellComponentProps = {
|
||||
...baseCellClientProps,
|
||||
cellData: 'name' in field ? doc[field.name] : undefined,
|
||||
link: isLinkedColumn,
|
||||
rowData: doc,
|
||||
}
|
||||
|
||||
const cellServerProps: DefaultServerCellComponentProps = {
|
||||
cellData: cellClientProps.cellData,
|
||||
className: baseCellClientProps.className,
|
||||
collectionConfig: payload.collections[collectionSlug].config,
|
||||
collectionSlug,
|
||||
columnIndex: baseCellClientProps.columnIndex,
|
||||
customCellProps: baseCellClientProps.customCellProps,
|
||||
field: _field,
|
||||
i18n,
|
||||
link: cellClientProps.link,
|
||||
onClick: baseCellClientProps.onClick,
|
||||
payload,
|
||||
rowData: doc,
|
||||
}
|
||||
|
||||
let CustomCell = null
|
||||
|
||||
if (_field?.type === 'richText') {
|
||||
if (!_field?.editor) {
|
||||
throw new MissingEditorProp(_field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof _field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
if (!_field.admin) {
|
||||
_field.admin = {}
|
||||
}
|
||||
|
||||
if (!_field.admin.components) {
|
||||
_field.admin.components = {}
|
||||
}
|
||||
|
||||
CustomCell = RenderServerComponent({
|
||||
clientProps: cellClientProps,
|
||||
Component: _field.editor.CellComponent,
|
||||
importMap: payload.importMap,
|
||||
serverProps: cellServerProps,
|
||||
})
|
||||
} else {
|
||||
const CustomCellComponent = _field?.admin?.components?.Cell
|
||||
|
||||
if (CustomCellComponent) {
|
||||
CustomCell = RenderServerComponent({
|
||||
clientProps: cellClientProps,
|
||||
Component: CustomCellComponent,
|
||||
importMap: payload.importMap,
|
||||
serverProps: cellServerProps,
|
||||
})
|
||||
} else {
|
||||
CustomCell = undefined
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={CustomCell}
|
||||
Fallback={
|
||||
<RenderDefaultCell
|
||||
clientProps={cellClientProps}
|
||||
columnIndex={index}
|
||||
enableRowSelections={enableRowSelections}
|
||||
isLinkedColumn={isLinkedColumn}
|
||||
/>
|
||||
}
|
||||
key={`${i}-${index}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: [],
|
||||
}
|
||||
|
||||
acc.push(column)
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
if (beforeRows) {
|
||||
sorted.unshift(...beforeRows)
|
||||
}
|
||||
|
||||
return sorted
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const useTableColumns = (): ITableColumns => useContext(TableColumnContex
|
||||
|
||||
type Props = {
|
||||
readonly children: React.ReactNode
|
||||
readonly collectionSlug: string
|
||||
readonly collectionSlug: string | string[]
|
||||
readonly columnState: Column[]
|
||||
readonly docs: any[]
|
||||
readonly enableRowSelections?: boolean
|
||||
@@ -68,7 +68,9 @@ export const TableColumnsProvider: React.FC<Props> = ({
|
||||
collectionSlug,
|
||||
})
|
||||
|
||||
const prevCollection = React.useRef<SanitizedCollectionConfig['slug']>(collectionSlug)
|
||||
const prevCollection = React.useRef<SanitizedCollectionConfig['slug']>(
|
||||
Array.isArray(collectionSlug) ? collectionSlug[0] : collectionSlug,
|
||||
)
|
||||
const { getPreference } = usePreferences()
|
||||
|
||||
const [tableColumns, setTableColumns] = React.useState(columnState)
|
||||
@@ -232,14 +234,15 @@ export const TableColumnsProvider: React.FC<Props> = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
const sync = async () => {
|
||||
const collectionHasChanged = prevCollection.current !== collectionSlug
|
||||
const defaultCollection = Array.isArray(collectionSlug) ? collectionSlug[0] : collectionSlug
|
||||
const collectionHasChanged = prevCollection.current !== defaultCollection
|
||||
|
||||
if (collectionHasChanged || !listPreferences) {
|
||||
const currentPreferences = await getPreference<{
|
||||
columns: ListPreferences['columns']
|
||||
}>(preferenceKey)
|
||||
|
||||
prevCollection.current = collectionSlug
|
||||
prevCollection.current = defaultCollection
|
||||
|
||||
if (currentPreferences?.columns) {
|
||||
// setTableColumns()
|
||||
|
||||
@@ -160,7 +160,9 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const where = {
|
||||
const where = Array.isArray(collection)
|
||||
? {}
|
||||
: {
|
||||
[on]: {
|
||||
equals: value,
|
||||
},
|
||||
@@ -173,10 +175,12 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
||||
}
|
||||
|
||||
return where
|
||||
}, [docID, field.targetField.relationTo, field.where, on, docConfig?.slug])
|
||||
}, [docID, collection, field.targetField.relationTo, field.where, on, docConfig?.slug])
|
||||
|
||||
const initialDrawerData = useMemo(() => {
|
||||
const relatedCollection = getEntityConfig({ collectionSlug: field.collection })
|
||||
const relatedCollection = getEntityConfig({
|
||||
collectionSlug: Array.isArray(field.collection) ? field.collection[0] : field.collection,
|
||||
})
|
||||
|
||||
return getInitialDrawerData({
|
||||
collectionSlug: docConfig?.slug,
|
||||
@@ -216,6 +220,15 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
||||
)}
|
||||
</h4>
|
||||
}
|
||||
parent={
|
||||
Array.isArray(collection)
|
||||
? {
|
||||
id: docID,
|
||||
collectionSlug: docConfig.slug,
|
||||
joinPath: path,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
relationTo={collection}
|
||||
/>
|
||||
<RenderCustomComponent
|
||||
|
||||
@@ -7,9 +7,10 @@ import type {
|
||||
ListPreferences,
|
||||
PaginatedDocs,
|
||||
SanitizedCollectionConfig,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
import { formatErrors } from 'payload'
|
||||
import { APIError, formatErrors } from 'payload'
|
||||
import { isNumber } from 'payload/shared'
|
||||
|
||||
import { getClientConfig } from './getClientConfig.js'
|
||||
@@ -73,6 +74,7 @@ export const buildTableState = async (
|
||||
columns,
|
||||
docs: docsFromArgs,
|
||||
enableRowSelections,
|
||||
parent,
|
||||
query,
|
||||
renderRowTypes,
|
||||
req,
|
||||
@@ -128,15 +130,19 @@ export const buildTableState = async (
|
||||
let collectionConfig: SanitizedCollectionConfig
|
||||
let clientCollectionConfig: ClientCollectionConfig
|
||||
|
||||
if (!Array.isArray(collectionSlug)) {
|
||||
if (req.payload.collections[collectionSlug]) {
|
||||
collectionConfig = req.payload.collections[collectionSlug].config
|
||||
clientCollectionConfig = clientConfig.collections.find(
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const listPreferences = await upsertPreferences<ListPreferences>({
|
||||
key: `${collectionSlug}-list`,
|
||||
key: Array.isArray(collectionSlug)
|
||||
? `${parent.collectionSlug}-${parent.joinPath}`
|
||||
: `${collectionSlug}-list`,
|
||||
req,
|
||||
value: {
|
||||
columns,
|
||||
@@ -151,6 +157,57 @@ export const buildTableState = async (
|
||||
// lookup docs, if desired, i.e. within `join` field which initialize with `depth: 0`
|
||||
|
||||
if (!docs || query) {
|
||||
if (Array.isArray(collectionSlug)) {
|
||||
if (!parent) {
|
||||
throw new APIError('Unexpected array of collectionSlug, parent must be providen')
|
||||
}
|
||||
|
||||
const select = {}
|
||||
let currentSelectRef = select
|
||||
|
||||
const segments = parent.joinPath.split('.')
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
currentSelectRef[segments[i]] = i === segments.length - 1 ? true : {}
|
||||
currentSelectRef = currentSelectRef[segments[i]]
|
||||
}
|
||||
|
||||
const joinQuery: { limit?: number; page?: number; sort?: string; where?: Where } = {
|
||||
sort: query?.sort as string,
|
||||
where: query?.where,
|
||||
}
|
||||
|
||||
if (query) {
|
||||
if (!Number.isNaN(Number(query.limit))) {
|
||||
joinQuery.limit = Number(query.limit)
|
||||
}
|
||||
|
||||
if (!Number.isNaN(Number(query.page))) {
|
||||
joinQuery.limit = Number(query.limit)
|
||||
}
|
||||
}
|
||||
|
||||
let parentDoc = await payload.findByID({
|
||||
id: parent.id,
|
||||
collection: parent.collectionSlug,
|
||||
depth: 1,
|
||||
joins: {
|
||||
[parent.joinPath]: joinQuery,
|
||||
},
|
||||
overrideAccess: false,
|
||||
select,
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (i === segments.length - 1) {
|
||||
data = parentDoc[segments[i]]
|
||||
docs = data.docs
|
||||
} else {
|
||||
parentDoc = parentDoc[segments[i]]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = await payload.find({
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
@@ -162,14 +219,16 @@ export const buildTableState = async (
|
||||
user: req.user,
|
||||
where: query?.where,
|
||||
})
|
||||
|
||||
docs = data.docs
|
||||
}
|
||||
}
|
||||
|
||||
const { columnState, Table } = renderTable({
|
||||
clientCollectionConfig,
|
||||
clientConfig,
|
||||
collectionConfig,
|
||||
columnPreferences: undefined, // TODO, might not be needed
|
||||
collections: Array.isArray(collectionSlug) ? collectionSlug : undefined,
|
||||
columnPreferences: Array.isArray(collectionSlug) ? listPreferences?.columns : undefined, // TODO, might not be neededcolumns,
|
||||
columns,
|
||||
docs,
|
||||
enableRowSelections,
|
||||
@@ -177,10 +236,16 @@ export const buildTableState = async (
|
||||
payload,
|
||||
renderRowTypes,
|
||||
tableAppearance,
|
||||
useAsTitle: collectionConfig.admin.useAsTitle,
|
||||
useAsTitle: Array.isArray(collectionSlug)
|
||||
? payload.collections[collectionSlug[0]]?.config?.admin?.useAsTitle
|
||||
: collectionConfig?.admin?.useAsTitle,
|
||||
})
|
||||
|
||||
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
|
||||
let renderedFilters
|
||||
|
||||
if (collectionConfig) {
|
||||
renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type {
|
||||
ClientCollectionConfig,
|
||||
ClientConfig,
|
||||
ClientField,
|
||||
CollectionConfig,
|
||||
Field,
|
||||
ImportMap,
|
||||
@@ -10,13 +12,14 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
||||
import { fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
|
||||
|
||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||
import type { Column } from '../exports/client/index.js'
|
||||
|
||||
import { RenderServerComponent } from '../elements/RenderServerComponent/index.js'
|
||||
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
|
||||
import { buildPolymorphicColumnState } from '../elements/TableColumns/buildPolymorphicColumnState.js'
|
||||
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
||||
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
||||
|
||||
@@ -50,7 +53,9 @@ export const renderFilters = (
|
||||
|
||||
export const renderTable = ({
|
||||
clientCollectionConfig,
|
||||
clientConfig,
|
||||
collectionConfig,
|
||||
collections,
|
||||
columnPreferences,
|
||||
columns: columnsFromArgs,
|
||||
customCellProps,
|
||||
@@ -62,8 +67,10 @@ export const renderTable = ({
|
||||
tableAppearance,
|
||||
useAsTitle,
|
||||
}: {
|
||||
clientCollectionConfig: ClientCollectionConfig
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
clientCollectionConfig?: ClientCollectionConfig
|
||||
clientConfig?: ClientConfig
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
collections?: string[]
|
||||
columnPreferences: ListPreferences['columns']
|
||||
columns?: ListPreferences['columns']
|
||||
customCellProps?: Record<string, any>
|
||||
@@ -80,6 +87,46 @@ export const renderTable = ({
|
||||
Table: React.ReactNode
|
||||
} => {
|
||||
// Ensure that columns passed as args comply with the field config, i.e. `hidden`, `disableListColumn`, etc.
|
||||
|
||||
let columnState: Column[]
|
||||
|
||||
if (collections) {
|
||||
const fields: ClientField[] = []
|
||||
for (const collection of collections) {
|
||||
const config = clientConfig.collections.find((each) => each.slug === collection)
|
||||
|
||||
for (const field of filterFields(config.fields)) {
|
||||
if (fieldAffectsData(field)) {
|
||||
if (fields.some((each) => fieldAffectsData(each) && each.name === field.name)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fields.push(field)
|
||||
}
|
||||
}
|
||||
|
||||
const columns = columnsFromArgs
|
||||
? columnsFromArgs?.filter((column) =>
|
||||
flattenTopLevelFields(fields, true)?.some(
|
||||
(field) => 'name' in field && field.name === column.accessor,
|
||||
),
|
||||
)
|
||||
: getInitialColumns(fields, useAsTitle, [])
|
||||
|
||||
columnState = buildPolymorphicColumnState({
|
||||
columnPreferences,
|
||||
columns,
|
||||
enableRowSelections,
|
||||
fields,
|
||||
i18n,
|
||||
// sortColumnProps,
|
||||
customCellProps,
|
||||
docs,
|
||||
payload,
|
||||
useAsTitle,
|
||||
})
|
||||
} else {
|
||||
const columns = columnsFromArgs
|
||||
? columnsFromArgs?.filter((column) =>
|
||||
flattenTopLevelFields(clientCollectionConfig.fields, true)?.some(
|
||||
@@ -92,7 +139,7 @@ export const renderTable = ({
|
||||
clientCollectionConfig?.admin?.defaultColumns,
|
||||
)
|
||||
|
||||
const columnState = buildColumnState({
|
||||
columnState = buildColumnState({
|
||||
clientCollectionConfig,
|
||||
collectionConfig,
|
||||
columnPreferences,
|
||||
@@ -105,6 +152,7 @@ export const renderTable = ({
|
||||
payload,
|
||||
useAsTitle,
|
||||
})
|
||||
}
|
||||
|
||||
const columnsToUse = [...columnState]
|
||||
|
||||
@@ -119,8 +167,15 @@ export const renderTable = ({
|
||||
hidden: true,
|
||||
},
|
||||
Heading: i18n.t('version:type'),
|
||||
renderedCells: docs.map((_, i) => (
|
||||
<Pill key={i}>{getTranslation(clientCollectionConfig.labels.singular, i18n)}</Pill>
|
||||
renderedCells: docs.map((doc, i) => (
|
||||
<Pill key={i}>
|
||||
{getTranslation(
|
||||
collections
|
||||
? payload.collections[doc.relationTo].config.labels.singular
|
||||
: clientCollectionConfig.labels.singular,
|
||||
i18n,
|
||||
)}
|
||||
</Pill>
|
||||
)),
|
||||
} as Column)
|
||||
}
|
||||
|
||||
@@ -220,6 +220,120 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'multiple-collections-parents',
|
||||
fields: [
|
||||
{
|
||||
type: 'join',
|
||||
name: 'children',
|
||||
collection: ['multiple-collections-1', 'multiple-collections-2'],
|
||||
on: 'parent',
|
||||
admin: {
|
||||
defaultColumns: ['title', 'name', 'description'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'multiple-collections-1',
|
||||
admin: { useAsTitle: 'title' },
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'multiple-collections-parents',
|
||||
name: 'parent',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'multiple-collections-2',
|
||||
admin: { useAsTitle: 'title' },
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'multiple-collections-parents',
|
||||
name: 'parent',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
slug: 'folders',
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'folders',
|
||||
name: 'folder',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
type: 'join',
|
||||
name: 'children',
|
||||
collection: ['folders', 'example-pages', 'example-posts'],
|
||||
on: 'folder',
|
||||
admin: {
|
||||
defaultColumns: ['title', 'name', 'description'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'example-pages',
|
||||
admin: { useAsTitle: 'title' },
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'folders',
|
||||
name: 'folder',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'example-posts',
|
||||
admin: { useAsTitle: 'title' },
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'folders',
|
||||
name: 'folder',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
localization: {
|
||||
locales: [
|
||||
|
||||
@@ -36,9 +36,11 @@ const { beforeAll, beforeEach, describe } = test
|
||||
describe('Join Field', () => {
|
||||
let page: Page
|
||||
let categoriesURL: AdminUrlUtil
|
||||
let foldersURL: AdminUrlUtil
|
||||
let uploadsURL: AdminUrlUtil
|
||||
let categoriesJoinRestrictedURL: AdminUrlUtil
|
||||
let categoryID: number | string
|
||||
let rootFolderID: number | string
|
||||
|
||||
beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
@@ -50,6 +52,7 @@ describe('Join Field', () => {
|
||||
categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug)
|
||||
uploadsURL = new AdminUrlUtil(serverURL, uploadsSlug)
|
||||
categoriesJoinRestrictedURL = new AdminUrlUtil(serverURL, categoriesJoinRestrictedSlug)
|
||||
foldersURL = new AdminUrlUtil(serverURL, 'folders')
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -86,6 +89,9 @@ describe('Join Field', () => {
|
||||
}
|
||||
|
||||
;({ id: categoryID } = docs[0])
|
||||
|
||||
const folder = await payload.find({ collection: 'folders', sort: 'createdAt', depth: 0 })
|
||||
rootFolderID = folder.docs[0]!.id
|
||||
})
|
||||
|
||||
test('should populate joined relationships in table cells of list view', async () => {
|
||||
@@ -469,6 +475,43 @@ describe('Join Field', () => {
|
||||
await expect(joinField.locator('.cell-canRead')).not.toContainText('false')
|
||||
})
|
||||
|
||||
test('should render join field with array of collections', async () => {
|
||||
await page.goto(foldersURL.edit(rootFolderID))
|
||||
const joinField = page.locator('#field-children.field-type.join')
|
||||
await expect(joinField).toBeVisible()
|
||||
await expect(
|
||||
joinField.locator('.relationship-table tbody .row-1 .cell-collection .pill__label'),
|
||||
).toHaveText('Folder')
|
||||
await expect(
|
||||
joinField.locator('.relationship-table tbody .row-3 .cell-collection .pill__label'),
|
||||
).toHaveText('Example Post')
|
||||
await expect(
|
||||
joinField.locator('.relationship-table tbody .row-5 .cell-collection .pill__label'),
|
||||
).toHaveText('Example Page')
|
||||
})
|
||||
|
||||
test('should create a new document from join field with array of collections', async () => {
|
||||
await page.goto(foldersURL.edit(rootFolderID))
|
||||
const joinField = page.locator('#field-children.field-type.join')
|
||||
await expect(joinField).toBeVisible()
|
||||
|
||||
const addNewPopupBtn = joinField.locator('.relationship-table__add-new-polymorphic')
|
||||
await expect(addNewPopupBtn).toBeVisible()
|
||||
await addNewPopupBtn.click()
|
||||
const pageOption = joinField.locator('.relationship-table__relation-button--example-pages')
|
||||
await expect(pageOption).toHaveText('Example Page')
|
||||
await pageOption.click()
|
||||
await page.locator('.drawer__content input#field-title').fill('Some new page')
|
||||
await page.locator('.drawer__content #action-save').click()
|
||||
|
||||
await expect(
|
||||
joinField.locator('.relationship-table tbody .row-1 .cell-collection .pill__label'),
|
||||
).toHaveText('Example Page')
|
||||
await expect(
|
||||
joinField.locator('.relationship-table tbody .row-1 .cell-title .drawer-link__cell'),
|
||||
).toHaveText('Some new page')
|
||||
})
|
||||
|
||||
test('should render create-first-user with when users collection has a join field and hide it', async () => {
|
||||
await payload.delete({ collection: 'users', where: {} })
|
||||
const url = new AdminUrlUtil(serverURL, 'users')
|
||||
|
||||
@@ -1153,6 +1153,123 @@ describe('Joins Field', () => {
|
||||
|
||||
expect(joinedDoc2.id).toBe(depthJoin_3.id)
|
||||
})
|
||||
|
||||
describe('Array of collection', () => {
|
||||
it('should join across multiple collections', async () => {
|
||||
let parent = await payload.create({
|
||||
collection: 'multiple-collections-parents',
|
||||
depth: 0,
|
||||
data: {},
|
||||
})
|
||||
|
||||
const child_1 = await payload.create({
|
||||
collection: 'multiple-collections-1',
|
||||
depth: 0,
|
||||
data: {
|
||||
parent,
|
||||
title: 'doc-1',
|
||||
},
|
||||
})
|
||||
|
||||
const child_2 = await payload.create({
|
||||
collection: 'multiple-collections-2',
|
||||
depth: 0,
|
||||
data: {
|
||||
parent,
|
||||
title: 'doc-2',
|
||||
},
|
||||
})
|
||||
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(parent.children.docs[0].value).toBe(child_2.id)
|
||||
expect(parent.children.docs[0]?.relationTo).toBe('multiple-collections-2')
|
||||
expect(parent.children.docs[1]?.value).toBe(child_1.id)
|
||||
expect(parent.children.docs[1]?.relationTo).toBe('multiple-collections-1')
|
||||
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 1,
|
||||
})
|
||||
|
||||
expect(parent.children.docs[0].value.id).toBe(child_2.id)
|
||||
expect(parent.children.docs[0]?.relationTo).toBe('multiple-collections-2')
|
||||
expect(parent.children.docs[1]?.value.id).toBe(child_1.id)
|
||||
expect(parent.children.docs[1]?.relationTo).toBe('multiple-collections-1')
|
||||
|
||||
// Sorting across collections
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 1,
|
||||
joins: {
|
||||
children: {
|
||||
sort: 'title',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(parent.children.docs[0]?.value.title).toBe('doc-1')
|
||||
expect(parent.children.docs[1]?.value.title).toBe('doc-2')
|
||||
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 1,
|
||||
joins: {
|
||||
children: {
|
||||
sort: '-title',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(parent.children.docs[0]?.value.title).toBe('doc-2')
|
||||
expect(parent.children.docs[1]?.value.title).toBe('doc-1')
|
||||
|
||||
// WHERE across collections
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 1,
|
||||
joins: {
|
||||
children: {
|
||||
where: {
|
||||
title: {
|
||||
equals: 'doc-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(parent.children?.docs).toHaveLength(1)
|
||||
expect(parent.children.docs[0]?.value.title).toBe('doc-1')
|
||||
|
||||
// WHERE by _relationTo (join for specific collectionSlug)
|
||||
parent = await payload.findByID({
|
||||
collection: 'multiple-collections-parents',
|
||||
id: parent.id,
|
||||
depth: 1,
|
||||
joins: {
|
||||
children: {
|
||||
where: {
|
||||
relationTo: {
|
||||
equals: 'multiple-collections-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(parent.children?.docs).toHaveLength(1)
|
||||
expect(parent.children.docs[0]?.value.title).toBe('doc-2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createPost(overrides?: Partial<Post>, locale?: Config['locale']) {
|
||||
|
||||
@@ -84,6 +84,12 @@ export interface Config {
|
||||
'depth-joins-1': DepthJoins1;
|
||||
'depth-joins-2': DepthJoins2;
|
||||
'depth-joins-3': DepthJoins3;
|
||||
'multiple-collections-parents': MultipleCollectionsParent;
|
||||
'multiple-collections-1': MultipleCollections1;
|
||||
'multiple-collections-2': MultipleCollections2;
|
||||
folders: Folder;
|
||||
'example-pages': ExamplePage;
|
||||
'example-posts': ExamplePost;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
@@ -135,6 +141,12 @@ export interface Config {
|
||||
'depth-joins-2': {
|
||||
joins: 'depth-joins-1';
|
||||
};
|
||||
'multiple-collections-parents': {
|
||||
children: 'multiple-collections-1' | 'multiple-collections-2';
|
||||
};
|
||||
folders: {
|
||||
children: 'folders' | 'example-pages' | 'example-posts';
|
||||
};
|
||||
};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
@@ -155,6 +167,12 @@ export interface Config {
|
||||
'depth-joins-1': DepthJoins1Select<false> | DepthJoins1Select<true>;
|
||||
'depth-joins-2': DepthJoins2Select<false> | DepthJoins2Select<true>;
|
||||
'depth-joins-3': DepthJoins3Select<false> | DepthJoins3Select<true>;
|
||||
'multiple-collections-parents': MultipleCollectionsParentsSelect<false> | MultipleCollectionsParentsSelect<true>;
|
||||
'multiple-collections-1': MultipleCollections1Select<false> | MultipleCollections1Select<true>;
|
||||
'multiple-collections-2': MultipleCollections2Select<false> | MultipleCollections2Select<true>;
|
||||
folders: FoldersSelect<false> | FoldersSelect<true>;
|
||||
'example-pages': ExamplePagesSelect<false> | ExamplePagesSelect<true>;
|
||||
'example-posts': ExamplePostsSelect<false> | ExamplePostsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -581,6 +599,108 @@ export interface DepthJoins3 {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "multiple-collections-parents".
|
||||
*/
|
||||
export interface MultipleCollectionsParent {
|
||||
id: string;
|
||||
children?: {
|
||||
docs?:
|
||||
| (
|
||||
| {
|
||||
relationTo?: 'multiple-collections-1';
|
||||
value: string | MultipleCollections1;
|
||||
}
|
||||
| {
|
||||
relationTo?: 'multiple-collections-2';
|
||||
value: string | MultipleCollections2;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
hasNextPage?: boolean | null;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "multiple-collections-1".
|
||||
*/
|
||||
export interface MultipleCollections1 {
|
||||
id: string;
|
||||
parent?: (string | null) | MultipleCollectionsParent;
|
||||
title?: string | null;
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "multiple-collections-2".
|
||||
*/
|
||||
export interface MultipleCollections2 {
|
||||
id: string;
|
||||
parent?: (string | null) | MultipleCollectionsParent;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "folders".
|
||||
*/
|
||||
export interface Folder {
|
||||
id: string;
|
||||
folder?: (string | null) | Folder;
|
||||
title?: string | null;
|
||||
children?: {
|
||||
docs?:
|
||||
| (
|
||||
| {
|
||||
relationTo?: 'folders';
|
||||
value: string | Folder;
|
||||
}
|
||||
| {
|
||||
relationTo?: 'example-pages';
|
||||
value: string | ExamplePage;
|
||||
}
|
||||
| {
|
||||
relationTo?: 'example-posts';
|
||||
value: string | ExamplePost;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
hasNextPage?: boolean | null;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "example-pages".
|
||||
*/
|
||||
export interface ExamplePage {
|
||||
id: string;
|
||||
folder?: (string | null) | Folder;
|
||||
title?: string | null;
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "example-posts".
|
||||
*/
|
||||
export interface ExamplePost {
|
||||
id: string;
|
||||
folder?: (string | null) | Folder;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
@@ -659,6 +779,30 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'depth-joins-3';
|
||||
value: string | DepthJoins3;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'multiple-collections-parents';
|
||||
value: string | MultipleCollectionsParent;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'multiple-collections-1';
|
||||
value: string | MultipleCollections1;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'multiple-collections-2';
|
||||
value: string | MultipleCollections2;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'folders';
|
||||
value: string | Folder;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'example-pages';
|
||||
value: string | ExamplePage;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'example-posts';
|
||||
value: string | ExamplePost;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -958,6 +1102,70 @@ export interface DepthJoins3Select<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "multiple-collections-parents_select".
|
||||
*/
|
||||
export interface MultipleCollectionsParentsSelect<T extends boolean = true> {
|
||||
children?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "multiple-collections-1_select".
|
||||
*/
|
||||
export interface MultipleCollections1Select<T extends boolean = true> {
|
||||
parent?: T;
|
||||
title?: T;
|
||||
name?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "multiple-collections-2_select".
|
||||
*/
|
||||
export interface MultipleCollections2Select<T extends boolean = true> {
|
||||
parent?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "folders_select".
|
||||
*/
|
||||
export interface FoldersSelect<T extends boolean = true> {
|
||||
folder?: T;
|
||||
title?: T;
|
||||
children?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "example-pages_select".
|
||||
*/
|
||||
export interface ExamplePagesSelect<T extends boolean = true> {
|
||||
folder?: T;
|
||||
title?: T;
|
||||
name?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "example-posts_select".
|
||||
*/
|
||||
export interface ExamplePostsSelect<T extends boolean = true> {
|
||||
folder?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
@@ -146,6 +146,74 @@ export const seed = async (_payload: Payload) => {
|
||||
category: restrictedCategory.id,
|
||||
},
|
||||
})
|
||||
|
||||
const root_folder = await _payload.create({
|
||||
collection: 'folders',
|
||||
data: {
|
||||
folder: null,
|
||||
title: 'Root folder',
|
||||
},
|
||||
})
|
||||
|
||||
const page_1 = await _payload.create({
|
||||
collection: 'example-pages',
|
||||
data: { title: 'page 1', name: 'Andrew', folder: root_folder },
|
||||
})
|
||||
|
||||
const post_1 = await _payload.create({
|
||||
collection: 'example-posts',
|
||||
data: { title: 'page 1', description: 'This is post 1', folder: root_folder },
|
||||
})
|
||||
|
||||
const page_2 = await _payload.create({
|
||||
collection: 'example-pages',
|
||||
data: { title: 'page 2', name: 'Sophia', folder: root_folder },
|
||||
})
|
||||
|
||||
const page_3 = await _payload.create({
|
||||
collection: 'example-pages',
|
||||
data: { title: 'page 3', name: 'Michael', folder: root_folder },
|
||||
})
|
||||
|
||||
const post_2 = await _payload.create({
|
||||
collection: 'example-posts',
|
||||
data: { title: 'post 2', description: 'This is post 2', folder: root_folder },
|
||||
})
|
||||
|
||||
const post_3 = await _payload.create({
|
||||
collection: 'example-posts',
|
||||
data: { title: 'post 3', description: 'This is post 3', folder: root_folder },
|
||||
})
|
||||
|
||||
const sub_folder_1 = await _payload.create({
|
||||
collection: 'folders',
|
||||
data: { folder: root_folder, title: 'Sub Folder 1' },
|
||||
})
|
||||
|
||||
const page_4 = await _payload.create({
|
||||
collection: 'example-pages',
|
||||
data: { title: 'page 4', name: 'Emma', folder: sub_folder_1 },
|
||||
})
|
||||
|
||||
const post_4 = await _payload.create({
|
||||
collection: 'example-posts',
|
||||
data: { title: 'post 4', description: 'This is post 4', folder: sub_folder_1 },
|
||||
})
|
||||
|
||||
const sub_folder_2 = await _payload.create({
|
||||
collection: 'folders',
|
||||
data: { folder: root_folder, title: 'Sub Folder 2' },
|
||||
})
|
||||
|
||||
const page_5 = await _payload.create({
|
||||
collection: 'example-pages',
|
||||
data: { title: 'page 5', name: 'Liam', folder: sub_folder_2 },
|
||||
})
|
||||
|
||||
const post_5 = await _payload.create({
|
||||
collection: 'example-posts',
|
||||
data: { title: 'post 5', description: 'This is post 5', folder: sub_folder_2 },
|
||||
})
|
||||
}
|
||||
|
||||
export async function clearAndSeedEverything(_payload: Payload) {
|
||||
|
||||
@@ -16,21 +16,13 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ES2022"
|
||||
],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"outDir": "${configDir}/dist",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"@types/jest"
|
||||
],
|
||||
"types": ["jest", "node", "@types/jest"],
|
||||
"incremental": true,
|
||||
"isolatedModules": true,
|
||||
"plugins": [
|
||||
@@ -51,33 +43,19 @@
|
||||
"@payloadcms/richtext-lexical/client": [
|
||||
"./packages/richtext-lexical/src/exports/client/index.ts"
|
||||
],
|
||||
"@payloadcms/richtext-lexical/rsc": [
|
||||
"./packages/richtext-lexical/src/exports/server/rsc.ts"
|
||||
],
|
||||
"@payloadcms/richtext-slate/rsc": [
|
||||
"./packages/richtext-slate/src/exports/server/rsc.ts"
|
||||
],
|
||||
"@payloadcms/richtext-lexical/rsc": ["./packages/richtext-lexical/src/exports/server/rsc.ts"],
|
||||
"@payloadcms/richtext-slate/rsc": ["./packages/richtext-slate/src/exports/server/rsc.ts"],
|
||||
"@payloadcms/richtext-slate/client": [
|
||||
"./packages/richtext-slate/src/exports/client/index.ts"
|
||||
],
|
||||
"@payloadcms/plugin-seo/client": [
|
||||
"./packages/plugin-seo/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-sentry/client": [
|
||||
"./packages/plugin-sentry/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-stripe/client": [
|
||||
"./packages/plugin-stripe/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-search/client": [
|
||||
"./packages/plugin-search/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-seo/client": ["./packages/plugin-seo/src/exports/client.ts"],
|
||||
"@payloadcms/plugin-sentry/client": ["./packages/plugin-sentry/src/exports/client.ts"],
|
||||
"@payloadcms/plugin-stripe/client": ["./packages/plugin-stripe/src/exports/client.ts"],
|
||||
"@payloadcms/plugin-search/client": ["./packages/plugin-search/src/exports/client.ts"],
|
||||
"@payloadcms/plugin-form-builder/client": [
|
||||
"./packages/plugin-form-builder/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-multi-tenant/rsc": [
|
||||
"./packages/plugin-multi-tenant/src/exports/rsc.ts"
|
||||
],
|
||||
"@payloadcms/plugin-multi-tenant/rsc": ["./packages/plugin-multi-tenant/src/exports/rsc.ts"],
|
||||
"@payloadcms/plugin-multi-tenant/utilities": [
|
||||
"./packages/plugin-multi-tenant/src/exports/utilities.ts"
|
||||
],
|
||||
@@ -87,21 +65,10 @@
|
||||
"@payloadcms/plugin-multi-tenant/client": [
|
||||
"./packages/plugin-multi-tenant/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-multi-tenant": [
|
||||
"./packages/plugin-multi-tenant/src/index.ts"
|
||||
],
|
||||
"@payloadcms/next": [
|
||||
"./packages/next/src/exports/*"
|
||||
]
|
||||
"@payloadcms/plugin-multi-tenant": ["./packages/plugin-multi-tenant/src/index.ts"],
|
||||
"@payloadcms/next": ["./packages/next/src/exports/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"${configDir}/src"
|
||||
],
|
||||
"exclude": [
|
||||
"${configDir}/dist",
|
||||
"${configDir}/build",
|
||||
"${configDir}/temp",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
"include": ["${configDir}/src"],
|
||||
"exclude": ["${configDir}/dist", "${configDir}/build", "${configDir}/temp", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user