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
|
// Load .git-blame-ignore-revs file
|
||||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
"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.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||||
|
"jestrunner.changeDirectoryToWorkspaceRoot": false,
|
||||||
"jestrunner.debugOptions": {
|
"jestrunner.debugOptions": {
|
||||||
"runtimeArgs": ["--no-deprecation"]
|
"runtimeArgs": ["--no-deprecation"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { flattenWhereToOperators } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
|
|
||||||
export const count: Count = async function count(
|
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'))
|
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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { CountOptions } from 'mongodb'
|
import type { CountOptions } from 'mongodb'
|
||||||
import type { CountGlobalVersions } from 'payload'
|
import type { CountGlobalVersions } from 'payload'
|
||||||
|
|
||||||
import { flattenWhereToOperators } from 'payload'
|
import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
|
|
||||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
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'))
|
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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { CountOptions } from 'mongodb'
|
import type { CountOptions } from 'mongodb'
|
||||||
import type { CountVersions } from 'payload'
|
import type { CountVersions } from 'payload'
|
||||||
|
|
||||||
import { flattenWhereToOperators } from 'payload'
|
import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
|
||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
|
|
||||||
export const countVersions: CountVersions = async function countVersions(
|
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'))
|
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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { DeleteMany } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
|
|
||||||
export const deleteMany: DeleteMany = async function deleteMany(
|
export const deleteMany: DeleteMany = async function deleteMany(
|
||||||
@@ -14,8 +15,10 @@ export const deleteMany: DeleteMany = async function deleteMany(
|
|||||||
session: await getSession(this, req),
|
session: await getSession(this, req),
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await Model.buildQuery({
|
const query = await buildQuery({
|
||||||
payload: this.payload,
|
adapter: this,
|
||||||
|
collectionSlug: collection,
|
||||||
|
fields: this.payload.collections[collection].config.flattenedFields,
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { DeleteOne, Document } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||||
@@ -21,8 +22,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
|||||||
session: await getSession(this, req),
|
session: await getSession(this, req),
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await Model.buildQuery({
|
const query = await buildQuery({
|
||||||
payload: this.payload,
|
adapter: this,
|
||||||
|
collectionSlug: collection,
|
||||||
|
fields: this.payload.collections[collection].config.flattenedFields,
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { DeleteVersions } from 'payload'
|
import { buildVersionCollectionFields, type DeleteVersions } from 'payload'
|
||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
|
|
||||||
export const deleteVersions: DeleteVersions = async function deleteVersions(
|
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 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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { flattenWhereToOperators } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildSortParam } from './queries/buildSortParam.js'
|
import { buildSortParam } from './queries/buildSortParam.js'
|
||||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { combineQueries } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||||
@@ -14,20 +15,22 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
|||||||
{ slug, locale, req, select, where },
|
{ slug, locale, req, select, where },
|
||||||
) {
|
) {
|
||||||
const Model = this.globals
|
const Model = this.globals
|
||||||
|
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
|
||||||
const options: QueryOptions = {
|
const options: QueryOptions = {
|
||||||
lean: true,
|
lean: true,
|
||||||
select: buildProjectionFromSelect({
|
select: buildProjectionFromSelect({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields,
|
fields,
|
||||||
select,
|
select,
|
||||||
}),
|
}),
|
||||||
session: await getSession(this, req),
|
session: await getSession(this, req),
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await Model.buildQuery({
|
const query = await buildQuery({
|
||||||
|
adapter: this,
|
||||||
|
fields,
|
||||||
globalSlug: slug,
|
globalSlug: slug,
|
||||||
locale,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where: combineQueries({ globalType: { equals: slug } }, where),
|
where: combineQueries({ globalType: { equals: slug } }, where),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildSortParam } from './queries/buildSortParam.js'
|
import { buildSortParam } from './queries/buildSortParam.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
@@ -46,10 +47,10 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await Model.buildQuery({
|
const query = await buildQuery({
|
||||||
globalSlug: global,
|
adapter: this,
|
||||||
|
fields: versionFields,
|
||||||
locale,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Document, FindOne } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
@@ -20,9 +21,11 @@ export const findOne: FindOne = async function findOne(
|
|||||||
session,
|
session,
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await Model.buildQuery({
|
const query = await buildQuery({
|
||||||
|
adapter: this,
|
||||||
|
collectionSlug: collection,
|
||||||
|
fields: collectionConfig.flattenedFields,
|
||||||
locale,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildSortParam } from './queries/buildSortParam.js'
|
import { buildSortParam } from './queries/buildSortParam.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -58,7 +62,7 @@ export const findVersions: FindVersions = async function findVersions(
|
|||||||
pagination,
|
pagination,
|
||||||
projection: buildProjectionFromSelect({
|
projection: buildProjectionFromSelect({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
fields,
|
||||||
select,
|
select,
|
||||||
}),
|
}),
|
||||||
sort,
|
sort,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { CollectionModel } from './types.js'
|
|||||||
import { buildCollectionSchema } from './models/buildCollectionSchema.js'
|
import { buildCollectionSchema } from './models/buildCollectionSchema.js'
|
||||||
import { buildGlobalModel } from './models/buildGlobalModel.js'
|
import { buildGlobalModel } from './models/buildGlobalModel.js'
|
||||||
import { buildSchema } from './models/buildSchema.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'
|
import { getDBName } from './utilities/getDBName.js'
|
||||||
|
|
||||||
export const init: Init = function init(this: MongooseAdapter) {
|
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 mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||||
import paginate from 'mongoose-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'
|
import { buildSchema } from './buildSchema.js'
|
||||||
|
|
||||||
export const buildCollectionSchema = (
|
export const buildCollectionSchema = (
|
||||||
@@ -44,7 +44,10 @@ export const buildCollectionSchema = (
|
|||||||
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
|
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
|
||||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
|
.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)
|
schema.plugin(mongooseAggregatePaginate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import mongoose from 'mongoose'
|
|||||||
|
|
||||||
import type { GlobalModel } from '../types.js'
|
import type { GlobalModel } from '../types.js'
|
||||||
|
|
||||||
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
|
import { getBuildQueryPlugin } from '../queries/getBuildQueryPlugin.js'
|
||||||
import { buildSchema } from './buildSchema.js'
|
import { buildSchema } from './buildSchema.js'
|
||||||
|
|
||||||
export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
|
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'
|
import { parseParams } from './parseParams.js'
|
||||||
|
|
||||||
type GetBuildQueryPluginArgs = {
|
export const buildQuery = async ({
|
||||||
collectionSlug?: string
|
adapter,
|
||||||
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 = ({
|
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
versionsFields,
|
fields,
|
||||||
}: GetBuildQueryPluginArgs = {}) => {
|
|
||||||
return function buildQueryPlugin(schema) {
|
|
||||||
const modifiedSchema = schema
|
|
||||||
async function buildQuery({
|
|
||||||
globalSlug,
|
globalSlug,
|
||||||
locale,
|
locale,
|
||||||
payload,
|
|
||||||
where,
|
where,
|
||||||
}: BuildQueryArgs): Promise<Record<string, unknown>> {
|
}: {
|
||||||
let fields = versionsFields
|
adapter: MongooseAdapter
|
||||||
if (!fields) {
|
collectionSlug?: string
|
||||||
if (globalSlug) {
|
fields: FlattenedField[]
|
||||||
const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug)
|
globalSlug?: string
|
||||||
fields = globalConfig.flattenedFields
|
locale?: string
|
||||||
}
|
where: Where
|
||||||
if (collectionSlug) {
|
}) => {
|
||||||
const collectionConfig = payload.collections[collectionSlug].config
|
|
||||||
fields = collectionConfig.flattenedFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const errors = []
|
|
||||||
const result = await parseParams({
|
const result = await parseParams({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
fields,
|
fields,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
locale,
|
locale,
|
||||||
parentIsLocalized: false,
|
parentIsLocalized: false,
|
||||||
payload,
|
payload: adapter.payload,
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
throw new QueryError(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
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 formattedValue = val
|
||||||
let formattedOperator = operator
|
let formattedOperator = operator
|
||||||
|
|
||||||
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
|
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
|
||||||
const segments = path.split('.')
|
const segments = path.split('.')
|
||||||
segments.shift()
|
segments.shift()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators }
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildSortParam } from './queries/buildSortParam.js'
|
import { buildSortParam } from './queries/buildSortParam.js'
|
||||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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 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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where: combinedWhere,
|
where: combinedWhere,
|
||||||
})
|
})
|
||||||
|
|
||||||
const projection = buildProjectionFromSelect({
|
const projection = buildProjectionFromSelect({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
fields,
|
||||||
select,
|
select,
|
||||||
})
|
})
|
||||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
// 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,
|
UploadField,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import type { BuildQueryArgs } from './queries/buildQuery.js'
|
import type { BuildQueryArgs } from './queries/getBuildQueryPlugin.js'
|
||||||
|
|
||||||
export interface CollectionModel
|
export interface CollectionModel
|
||||||
extends Model<any>,
|
extends Model<any>,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.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 currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
|
||||||
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
|
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
|
||||||
|
const flattenedFields = buildVersionGlobalFields(this.payload.config, currentGlobal, true)
|
||||||
const options: QueryOptions = {
|
const options: QueryOptions = {
|
||||||
...optionsArgs,
|
...optionsArgs,
|
||||||
lean: true,
|
lean: true,
|
||||||
new: true,
|
new: true,
|
||||||
projection: buildProjectionFromSelect({
|
projection: buildProjectionFromSelect({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true),
|
fields: flattenedFields,
|
||||||
select,
|
select,
|
||||||
}),
|
}),
|
||||||
session: await getSession(this, req),
|
session: await getSession(this, req),
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await VersionModel.buildQuery({
|
const query = await buildQuery({
|
||||||
|
adapter: this,
|
||||||
|
fields: flattenedFields,
|
||||||
locale,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where: whereToUse,
|
where: whereToUse,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { UpdateOne } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
import { handleError } from './utilities/handleError.js'
|
import { handleError } from './utilities/handleError.js'
|
||||||
@@ -28,9 +29,11 @@ export const updateOne: UpdateOne = async function updateOne(
|
|||||||
session: await getSession(this, req),
|
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,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
|
|||||||
|
|
||||||
import type { MongooseAdapter } from './index.js'
|
import type { MongooseAdapter } from './index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||||
import { getSession } from './utilities/getSession.js'
|
import { getSession } from './utilities/getSession.js'
|
||||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||||
@@ -19,25 +20,28 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
|||||||
this.payload.collections[collection].config,
|
this.payload.collections[collection].config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const flattenedFields = buildVersionCollectionFields(
|
||||||
|
this.payload.config,
|
||||||
|
this.payload.collections[collection].config,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
const options: QueryOptions = {
|
const options: QueryOptions = {
|
||||||
...optionsArgs,
|
...optionsArgs,
|
||||||
lean: true,
|
lean: true,
|
||||||
new: true,
|
new: true,
|
||||||
projection: buildProjectionFromSelect({
|
projection: buildProjectionFromSelect({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
fields: buildVersionCollectionFields(
|
fields: flattenedFields,
|
||||||
this.payload.config,
|
|
||||||
this.payload.collections[collection].config,
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
select,
|
select,
|
||||||
}),
|
}),
|
||||||
session: await getSession(this, req),
|
session: await getSession(this, req),
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await VersionModel.buildQuery({
|
const query = await buildQuery({
|
||||||
|
adapter: this,
|
||||||
|
fields: flattenedFields,
|
||||||
locale,
|
locale,
|
||||||
payload: this.payload,
|
|
||||||
where: whereToUse,
|
where: whereToUse,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import type { PipelineStage } from 'mongoose'
|
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 { fieldShouldBeLocalized } from 'payload/shared'
|
||||||
|
|
||||||
import type { MongooseAdapter } from '../index.js'
|
import type { MongooseAdapter } from '../index.js'
|
||||||
|
|
||||||
|
import { buildQuery } from '../queries/buildQuery.js'
|
||||||
import { buildSortParam } from '../queries/buildSortParam.js'
|
import { buildSortParam } from '../queries/buildSortParam.js'
|
||||||
|
|
||||||
type BuildJoinAggregationArgs = {
|
type BuildJoinAggregationArgs = {
|
||||||
@@ -33,11 +40,16 @@ export const buildJoinAggregation = async ({
|
|||||||
query,
|
query,
|
||||||
versions,
|
versions,
|
||||||
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
|
}: 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const joinConfig = adapter.payload.collections[collection].config.joins
|
const joinConfig = adapter.payload.collections[collection].config.joins
|
||||||
|
const polymorphicJoinsConfig = adapter.payload.collections[collection].config.polymorphicJoins
|
||||||
const aggregate: PipelineStage[] = [
|
const aggregate: PipelineStage[] = [
|
||||||
{
|
{
|
||||||
$sort: { createdAt: -1 },
|
$sort: { createdAt: -1 },
|
||||||
@@ -56,10 +68,7 @@ export const buildJoinAggregation = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const slug of Object.keys(joinConfig)) {
|
for (const join of polymorphicJoinsConfig) {
|
||||||
for (const join of joinConfig[slug]) {
|
|
||||||
const joinModel = adapter.collections[join.field.collection]
|
|
||||||
|
|
||||||
if (projection && !projection[join.joinPath]) {
|
if (projection && !projection[join.joinPath]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -75,6 +84,156 @@ export const buildJoinAggregation = async ({
|
|||||||
where: whereJoin,
|
where: whereJoin,
|
||||||
} = joins?.[join.joinPath] || {}
|
} = 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({
|
const sort = buildSortParam({
|
||||||
config: adapter.payload.config,
|
config: adapter.payload.config,
|
||||||
fields: adapter.payload.collections[slug].config.flattenedFields,
|
fields: adapter.payload.collections[slug].config.flattenedFields,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
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 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 { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||||
import toSnakeCase from 'to-snake-case'
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
@@ -10,11 +11,49 @@ import type { Result } from './buildFindManyArgs.js'
|
|||||||
|
|
||||||
import buildQuery from '../queries/buildQuery.js'
|
import buildQuery from '../queries/buildQuery.js'
|
||||||
import { getTableAlias } from '../queries/getTableAlias.js'
|
import { getTableAlias } from '../queries/getTableAlias.js'
|
||||||
|
import { operatorMap } from '../queries/operatorMap.js'
|
||||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||||
import { jsonAggBuildObject } from '../utilities/json.js'
|
import { jsonAggBuildObject } from '../utilities/json.js'
|
||||||
import { rawConstraint } from '../utilities/rawConstraint.js'
|
import { rawConstraint } from '../utilities/rawConstraint.js'
|
||||||
import { chainMethods } from './chainMethods.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 = {
|
type TraverseFieldArgs = {
|
||||||
_locales: Result
|
_locales: Result
|
||||||
adapter: DrizzleAdapter
|
adapter: DrizzleAdapter
|
||||||
@@ -359,6 +398,111 @@ export const traverseFields = ({
|
|||||||
limit += 1
|
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 fields = adapter.payload.collections[field.collection].config.flattenedFields
|
||||||
|
|
||||||
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
|
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(field.collection))
|
||||||
@@ -413,9 +557,7 @@ export const traverseFields = ({
|
|||||||
fields,
|
fields,
|
||||||
joins,
|
joins,
|
||||||
locale,
|
locale,
|
||||||
// Parent is never localized, as we're passing the `fields` of a **different** collection here. This means that the
|
parentIsLocalized,
|
||||||
// parent localization "boundary" is crossed, and we're now in the context of the joined collection.
|
|
||||||
parentIsLocalized: false,
|
|
||||||
selectLocale: true,
|
selectLocale: true,
|
||||||
sort,
|
sort,
|
||||||
tableName: joinCollectionTableName,
|
tableName: joinCollectionTableName,
|
||||||
@@ -431,13 +573,6 @@ export const traverseFields = ({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (limit !== 0) {
|
|
||||||
chainedMethods.push({
|
|
||||||
args: [limit],
|
|
||||||
method: 'limit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page && limit !== 0) {
|
if (page && limit !== 0) {
|
||||||
const offset = (page - 1) * limit - 1
|
const offset = (page - 1) * limit - 1
|
||||||
if (offset > 0) {
|
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
|
const db = adapter.drizzle as LibSQLDatabase
|
||||||
|
|
||||||
for (let key in selectFields) {
|
for (let key in selectFields) {
|
||||||
@@ -479,6 +621,7 @@ export const traverseFields = ({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.from(sql`${subQuery}`)}`.as(subQueryAlias)
|
.from(sql`${subQuery}`)}`.as(subQueryAlias)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,9 +436,14 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
} else {
|
} else {
|
||||||
const hasNextPage = limit !== 0 && fieldData.length > limit
|
const hasNextPage = limit !== 0 && fieldData.length > limit
|
||||||
fieldResult = {
|
fieldResult = {
|
||||||
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(({ id }) => ({
|
docs: (hasNextPage ? fieldData.slice(0, limit) : fieldData).map(
|
||||||
id,
|
({ id, relationTo }) => {
|
||||||
})),
|
if (relationTo) {
|
||||||
|
return { relationTo, value: id }
|
||||||
|
}
|
||||||
|
return { id }
|
||||||
|
},
|
||||||
|
),
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,9 @@ export function buildObjectType({
|
|||||||
name: joinName,
|
name: joinName,
|
||||||
fields: {
|
fields: {
|
||||||
docs: {
|
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 },
|
hasNextPage: { type: GraphQLBoolean },
|
||||||
},
|
},
|
||||||
@@ -270,7 +272,9 @@ export function buildObjectType({
|
|||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
type: graphqlResult.collections[field.collection].graphQL.whereInputType,
|
type: Array.isArray(field.collection)
|
||||||
|
? GraphQLJSON
|
||||||
|
: graphqlResult.collections[field.collection].graphQL.whereInputType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
@@ -286,6 +290,10 @@ export function buildObjectType({
|
|||||||
[field.on]: { equals: parent._id ?? parent.id },
|
[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({
|
return await req.payload.find({
|
||||||
collection,
|
collection,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ImportMap } from '../../bin/generateImportMap/index.js'
|
import type { ImportMap } from '../../bin/generateImportMap/index.js'
|
||||||
import type { SanitizedConfig } from '../../config/types.js'
|
import type { SanitizedConfig } from '../../config/types.js'
|
||||||
import type { PaginatedDocs } from '../../database/types.js'
|
import type { PaginatedDocs } from '../../database/types.js'
|
||||||
|
import type { CollectionSlug } from '../../index.js'
|
||||||
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
|
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
|
||||||
|
|
||||||
export type DefaultServerFunctionArgs = {
|
export type DefaultServerFunctionArgs = {
|
||||||
@@ -48,10 +49,15 @@ export type ListQuery = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type BuildTableStateArgs = {
|
export type BuildTableStateArgs = {
|
||||||
collectionSlug: string
|
collectionSlug: string | string[]
|
||||||
columns?: { accessor: string; active: boolean }[]
|
columns?: { accessor: string; active: boolean }[]
|
||||||
docs?: PaginatedDocs['docs']
|
docs?: PaginatedDocs['docs']
|
||||||
enableRowSelections?: boolean
|
enableRowSelections?: boolean
|
||||||
|
parent?: {
|
||||||
|
collectionSlug: CollectionSlug
|
||||||
|
id: number | string
|
||||||
|
joinPath: string
|
||||||
|
}
|
||||||
query?: ListQuery
|
query?: ListQuery
|
||||||
renderRowTypes?: boolean
|
renderRowTypes?: boolean
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { createClientFields } from '../../fields/config/client.js'
|
|||||||
|
|
||||||
export type ServerOnlyCollectionProperties = keyof Pick<
|
export type ServerOnlyCollectionProperties = keyof Pick<
|
||||||
SanitizedCollectionConfig,
|
SanitizedCollectionConfig,
|
||||||
'access' | 'custom' | 'endpoints' | 'flattenedFields' | 'hooks' | 'joins'
|
'access' | 'custom' | 'endpoints' | 'flattenedFields' | 'hooks' | 'joins' | 'polymorphicJoins'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type ServerOnlyCollectionAdminProperties = keyof Pick<
|
export type ServerOnlyCollectionAdminProperties = keyof Pick<
|
||||||
@@ -68,6 +68,7 @@ const serverOnlyCollectionProperties: Partial<ServerOnlyCollectionProperties>[]
|
|||||||
'endpoints',
|
'endpoints',
|
||||||
'custom',
|
'custom',
|
||||||
'joins',
|
'joins',
|
||||||
|
'polymorphicJoins',
|
||||||
'flattenedFields',
|
'flattenedFields',
|
||||||
// `upload`
|
// `upload`
|
||||||
// `admin`
|
// `admin`
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import type { LoginWithUsernameOptions } from '../../auth/types.js'
|
import type { LoginWithUsernameOptions } from '../../auth/types.js'
|
||||||
import type { Config, SanitizedConfig } from '../../config/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 { authCollectionEndpoints } from '../../auth/endpoints/index.js'
|
||||||
import { getBaseAuthFields } from '../../auth/getAuthFields.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 validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
|
||||||
|
|
||||||
const joins: SanitizedJoins = {}
|
const joins: SanitizedJoins = {}
|
||||||
|
const polymorphicJoins: SanitizedJoin[] = []
|
||||||
sanitized.fields = await sanitizeFields({
|
sanitized.fields = await sanitizeFields({
|
||||||
collectionConfig: sanitized,
|
collectionConfig: sanitized,
|
||||||
config,
|
config,
|
||||||
@@ -51,6 +57,7 @@ export const sanitizeCollection = async (
|
|||||||
joinPath: '',
|
joinPath: '',
|
||||||
joins,
|
joins,
|
||||||
parentIsLocalized: false,
|
parentIsLocalized: false,
|
||||||
|
polymorphicJoins,
|
||||||
richTextSanitizationPromises,
|
richTextSanitizationPromises,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
})
|
})
|
||||||
@@ -234,6 +241,7 @@ export const sanitizeCollection = async (
|
|||||||
const sanitizedConfig = sanitized as SanitizedCollectionConfig
|
const sanitizedConfig = sanitized as SanitizedCollectionConfig
|
||||||
|
|
||||||
sanitizedConfig.joins = joins
|
sanitizedConfig.joins = joins
|
||||||
|
sanitizedConfig.polymorphicJoins = polymorphicJoins
|
||||||
|
|
||||||
sanitizedConfig.flattenedFields = flattenAllFields({ fields: sanitizedConfig.fields })
|
sanitizedConfig.flattenedFields = flattenAllFields({ fields: sanitizedConfig.fields })
|
||||||
|
|
||||||
|
|||||||
@@ -548,6 +548,12 @@ export interface SanitizedCollectionConfig
|
|||||||
* Object of collections to join 'Join Fields object keyed by collection
|
* Object of collections to join 'Join Fields object keyed by collection
|
||||||
*/
|
*/
|
||||||
joins: SanitizedJoins
|
joins: SanitizedJoins
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of all polymorphic join fields
|
||||||
|
*/
|
||||||
|
polymorphicJoins: SanitizedJoin[]
|
||||||
|
|
||||||
slug: CollectionSlug
|
slug: CollectionSlug
|
||||||
upload: SanitizedUploadConfig
|
upload: SanitizedUploadConfig
|
||||||
versions: SanitizedCollectionVersions
|
versions: SanitizedCollectionVersions
|
||||||
|
|||||||
@@ -107,6 +107,18 @@ export function getLocalizedPaths({
|
|||||||
return paths
|
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) {
|
if (!matchedField && currentPath === 'id' && i === pathSegments.length - 1) {
|
||||||
lastIncompletePath.path = currentPath
|
lastIncompletePath.path = currentPath
|
||||||
const idField: Field = {
|
const idField: Field = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-strict-ignore
|
// @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 type { JoinQuery, PayloadRequest } from '../types/index.js'
|
||||||
|
|
||||||
import executeAccess from '../auth/executeAccess.js'
|
import executeAccess from '../auth/executeAccess.js'
|
||||||
@@ -14,6 +14,70 @@ type Args = {
|
|||||||
req: PayloadRequest
|
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
|
* * Validates `where` for each join
|
||||||
* * Combines the access result for joined collection
|
* * Combines the access result for joined collection
|
||||||
@@ -37,51 +101,31 @@ export const sanitizeJoinQuery = async ({
|
|||||||
const promises: Promise<void>[] = []
|
const promises: Promise<void>[] = []
|
||||||
|
|
||||||
for (const collectionSlug in collectionConfig.joins) {
|
for (const collectionSlug in collectionConfig.joins) {
|
||||||
for (const { field, joinPath } of collectionConfig.joins[collectionSlug]) {
|
for (const join of collectionConfig.joins[collectionSlug]) {
|
||||||
if (joinsQuery[joinPath] === false) {
|
await sanitizeJoinFieldQuery({
|
||||||
continue
|
collectionSlug,
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
errors,
|
errors,
|
||||||
|
join,
|
||||||
|
joinsQuery,
|
||||||
overrideAccess,
|
overrideAccess,
|
||||||
|
promises,
|
||||||
req,
|
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)
|
await Promise.all(promises)
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export const createClientField = ({
|
|||||||
const field = clientField as JoinFieldClient
|
const field = clientField as JoinFieldClient
|
||||||
|
|
||||||
field.targetField = {
|
field.targetField = {
|
||||||
relationTo: field.targetField.relationTo,
|
relationTo: field.targetField?.relationTo,
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { deepMergeSimple } from '@payloadcms/translations/utilities'
|
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 { Config, SanitizedConfig } from '../../config/types.js'
|
||||||
import type { Field } from './types.js'
|
import type { Field } from './types.js'
|
||||||
|
|
||||||
@@ -33,6 +37,7 @@ type Args = {
|
|||||||
*/
|
*/
|
||||||
joins?: SanitizedJoins
|
joins?: SanitizedJoins
|
||||||
parentIsLocalized: boolean
|
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.
|
* 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 = '',
|
joinPath = '',
|
||||||
joins,
|
joins,
|
||||||
parentIsLocalized,
|
parentIsLocalized,
|
||||||
|
polymorphicJoins,
|
||||||
requireFieldLevelRichTextEditor = false,
|
requireFieldLevelRichTextEditor = false,
|
||||||
richTextSanitizationPromises,
|
richTextSanitizationPromises,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
@@ -104,7 +110,7 @@ export const sanitizeFields = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'join') {
|
if (field.type === 'join') {
|
||||||
sanitizeJoinField({ config, field, joinPath, joins, parentIsLocalized })
|
sanitizeJoinField({ config, field, joinPath, joins, parentIsLocalized, polymorphicJoins })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'relationship' || field.type === 'upload') {
|
if (field.type === 'relationship' || field.type === 'upload') {
|
||||||
@@ -265,6 +271,7 @@ export const sanitizeFields = async ({
|
|||||||
: joinPath,
|
: joinPath,
|
||||||
joins,
|
joins,
|
||||||
parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
|
parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
|
||||||
|
polymorphicJoins,
|
||||||
requireFieldLevelRichTextEditor,
|
requireFieldLevelRichTextEditor,
|
||||||
richTextSanitizationPromises,
|
richTextSanitizationPromises,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
@@ -285,6 +292,7 @@ export const sanitizeFields = async ({
|
|||||||
joinPath: tabHasName(tab) ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
|
joinPath: tabHasName(tab) ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
|
||||||
joins,
|
joins,
|
||||||
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
||||||
|
polymorphicJoins,
|
||||||
requireFieldLevelRichTextEditor,
|
requireFieldLevelRichTextEditor,
|
||||||
richTextSanitizationPromises,
|
richTextSanitizationPromises,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ export const sanitizeJoinField = ({
|
|||||||
joinPath,
|
joinPath,
|
||||||
joins,
|
joins,
|
||||||
parentIsLocalized,
|
parentIsLocalized,
|
||||||
|
polymorphicJoins,
|
||||||
|
validateOnly,
|
||||||
}: {
|
}: {
|
||||||
config: Config
|
config: Config
|
||||||
field: FlattenedJoinField | JoinField
|
field: FlattenedJoinField | JoinField
|
||||||
joinPath?: string
|
joinPath?: string
|
||||||
joins?: SanitizedJoins
|
joins?: SanitizedJoins
|
||||||
parentIsLocalized: boolean
|
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
|
// the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field
|
||||||
if (typeof joins === 'undefined') {
|
if (typeof joins === 'undefined') {
|
||||||
@@ -38,6 +42,32 @@ export const sanitizeJoinField = ({
|
|||||||
parentIsLocalized,
|
parentIsLocalized,
|
||||||
targetField: undefined,
|
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(
|
const joinCollection = config.collections.find(
|
||||||
(collection) => collection.slug === field.collection,
|
(collection) => collection.slug === field.collection,
|
||||||
)
|
)
|
||||||
@@ -109,6 +139,10 @@ export const sanitizeJoinField = ({
|
|||||||
throw new InvalidFieldJoin(join.field)
|
throw new InvalidFieldJoin(join.field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
join.targetField = joinRelationship
|
join.targetField = joinRelationship
|
||||||
|
|
||||||
// override the join field localized property to use whatever the relationship field has
|
// 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.
|
* The slug of the collection to relate with.
|
||||||
*/
|
*/
|
||||||
collection: CollectionSlug
|
collection: CollectionSlug | CollectionSlug[]
|
||||||
defaultLimit?: number
|
defaultLimit?: number
|
||||||
defaultSort?: Sort
|
defaultSort?: Sort
|
||||||
defaultValue?: never
|
defaultValue?: never
|
||||||
@@ -1504,6 +1504,7 @@ export type JoinField = {
|
|||||||
* A string for the field in the collection being joined to.
|
* A string for the field in the collection being joined to.
|
||||||
*/
|
*/
|
||||||
on: string
|
on: string
|
||||||
|
sanitizedMany?: JoinField[]
|
||||||
type: 'join'
|
type: 'join'
|
||||||
validate?: never
|
validate?: never
|
||||||
where?: Where
|
where?: Where
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type PopulateArgs = {
|
|||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this function is mess, refactor logic
|
||||||
const populate = async ({
|
const populate = async ({
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data,
|
data,
|
||||||
@@ -41,14 +42,24 @@ const populate = async ({
|
|||||||
const dataToUpdate = dataReference
|
const dataToUpdate = dataReference
|
||||||
let relation
|
let relation
|
||||||
if (field.type === 'join') {
|
if (field.type === 'join') {
|
||||||
relation = field.collection
|
relation = Array.isArray(field.collection) ? data.relationTo : field.collection
|
||||||
} else {
|
} else {
|
||||||
relation = Array.isArray(field.relationTo) ? (data.relationTo as string) : field.relationTo
|
relation = Array.isArray(field.relationTo) ? (data.relationTo as string) : field.relationTo
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedCollection = req.payload.collections[relation]
|
const relatedCollection = req.payload.collections[relation]
|
||||||
|
|
||||||
if (relatedCollection) {
|
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
|
let relationshipValue
|
||||||
const shouldPopulate = depth && currentDepth <= depth
|
const shouldPopulate = depth && currentDepth <= depth
|
||||||
|
|
||||||
@@ -89,12 +100,20 @@ const populate = async ({
|
|||||||
if (typeof index === 'number' && typeof key === 'string') {
|
if (typeof index === 'number' && typeof key === 'string') {
|
||||||
if (field.type !== 'join' && Array.isArray(field.relationTo)) {
|
if (field.type !== 'join' && Array.isArray(field.relationTo)) {
|
||||||
dataToUpdate[field.name][key][index].value = relationshipValue
|
dataToUpdate[field.name][key][index].value = relationshipValue
|
||||||
|
} else {
|
||||||
|
if (field.type === 'join' && Array.isArray(field.collection)) {
|
||||||
|
dataToUpdate[field.name][key][index].value = relationshipValue
|
||||||
} else {
|
} else {
|
||||||
dataToUpdate[field.name][key][index] = relationshipValue
|
dataToUpdate[field.name][key][index] = relationshipValue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (typeof index === 'number' || typeof key === 'string') {
|
} else if (typeof index === 'number' || typeof key === 'string') {
|
||||||
if (field.type === 'join') {
|
if (field.type === 'join') {
|
||||||
|
if (!Array.isArray(field.collection)) {
|
||||||
dataToUpdate[field.name].docs[index ?? key] = relationshipValue
|
dataToUpdate[field.name].docs[index ?? key] = relationshipValue
|
||||||
|
} else {
|
||||||
|
dataToUpdate[field.name].docs[index ?? key].value = relationshipValue
|
||||||
|
}
|
||||||
} else if (Array.isArray(field.relationTo)) {
|
} else if (Array.isArray(field.relationTo)) {
|
||||||
dataToUpdate[field.name][index ?? key].value = relationshipValue
|
dataToUpdate[field.name][index ?? key].value = relationshipValue
|
||||||
} else {
|
} else {
|
||||||
@@ -102,11 +121,15 @@ const populate = async ({
|
|||||||
}
|
}
|
||||||
} else if (field.type !== 'join' && Array.isArray(field.relationTo)) {
|
} else if (field.type !== 'join' && Array.isArray(field.relationTo)) {
|
||||||
dataToUpdate[field.name].value = relationshipValue
|
dataToUpdate[field.name].value = relationshipValue
|
||||||
|
} else {
|
||||||
|
if (field.type === 'join' && Array.isArray(field.collection)) {
|
||||||
|
dataToUpdate[field.name].value = relationshipValue
|
||||||
} else {
|
} else {
|
||||||
dataToUpdate[field.name] = relationshipValue
|
dataToUpdate[field.name] = relationshipValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type PromiseArgs = {
|
type PromiseArgs = {
|
||||||
currentDepth: number
|
currentDepth: number
|
||||||
@@ -185,7 +208,10 @@ export const relationshipPopulationPromise = async ({
|
|||||||
if (relatedDoc) {
|
if (relatedDoc) {
|
||||||
await populate({
|
await populate({
|
||||||
currentDepth,
|
currentDepth,
|
||||||
data: relatedDoc?.id ? relatedDoc.id : relatedDoc,
|
data:
|
||||||
|
!(field.type === 'join' && Array.isArray(field.collection)) && relatedDoc?.id
|
||||||
|
? relatedDoc.id
|
||||||
|
: relatedDoc,
|
||||||
dataReference: resultingDoc,
|
dataReference: resultingDoc,
|
||||||
depth: populateDepth,
|
depth: populateDepth,
|
||||||
draft,
|
draft,
|
||||||
|
|||||||
@@ -1246,6 +1246,7 @@ export type {
|
|||||||
FlattenedBlocksField,
|
FlattenedBlocksField,
|
||||||
FlattenedField,
|
FlattenedField,
|
||||||
FlattenedGroupField,
|
FlattenedGroupField,
|
||||||
|
FlattenedJoinField,
|
||||||
FlattenedTabAsField,
|
FlattenedTabAsField,
|
||||||
GroupField,
|
GroupField,
|
||||||
GroupFieldClient,
|
GroupFieldClient,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ function generateEntitySelectSchemas(
|
|||||||
|
|
||||||
function generateCollectionJoinsSchemas(collections: SanitizedCollectionConfig[]): JSONSchema4 {
|
function generateCollectionJoinsSchemas(collections: SanitizedCollectionConfig[]): JSONSchema4 {
|
||||||
const properties = [...collections].reduce<Record<string, JSONSchema4>>(
|
const properties = [...collections].reduce<Record<string, JSONSchema4>>(
|
||||||
(acc, { slug, joins }) => {
|
(acc, { slug, joins, polymorphicJoins }) => {
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
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) {
|
if (Object.keys(schema.properties).length > 0) {
|
||||||
acc[slug] = schema
|
acc[slug] = schema
|
||||||
}
|
}
|
||||||
@@ -387,14 +395,33 @@ export function fieldsToJSONSchema(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'join': {
|
case 'join': {
|
||||||
fieldSchema = {
|
let items: JSONSchema4
|
||||||
...baseFieldSchema,
|
|
||||||
type: withNullableJSONSchemaType('object', false),
|
if (Array.isArray(field.collection)) {
|
||||||
|
items = {
|
||||||
|
oneOf: field.collection.map((collection) => ({
|
||||||
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
docs: {
|
relationTo: {
|
||||||
type: withNullableJSONSchemaType('array', false),
|
const: collection,
|
||||||
items: {
|
},
|
||||||
|
value: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: collectionIDFieldTypes[collection],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$ref: `#/definitions/${collection}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['collectionSlug', 'value'],
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items = {
|
||||||
oneOf: [
|
oneOf: [
|
||||||
{
|
{
|
||||||
type: collectionIDFieldTypes[field.collection],
|
type: collectionIDFieldTypes[field.collection],
|
||||||
@@ -403,7 +430,17 @@ export function fieldsToJSONSchema(
|
|||||||
$ref: `#/definitions/${field.collection}`,
|
$ref: `#/definitions/${field.collection}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldSchema = {
|
||||||
|
...baseFieldSchema,
|
||||||
|
type: withNullableJSONSchemaType('object', false),
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
docs: {
|
||||||
|
type: withNullableJSONSchemaType('array', false),
|
||||||
|
items,
|
||||||
},
|
},
|
||||||
hasNextPage: { type: withNullableJSONSchemaType('boolean', false) },
|
hasNextPage: { type: withNullableJSONSchemaType('boolean', false) },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,12 @@
|
|||||||
padding-bottom: var(--base);
|
padding-bottom: var(--base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__add-new-polymorphic .btn__label {
|
||||||
|
display: flex;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
'use client'
|
'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 { getTranslation } from '@payloadcms/translations'
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
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 { Pill } from '../../elements/Pill/index.js'
|
||||||
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
|
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
|
||||||
import { ChevronIcon } from '../../icons/Chevron/index.js'
|
import { ChevronIcon } from '../../icons/Chevron/index.js'
|
||||||
|
import { PlusIcon } from '../../icons/Plus/index.js'
|
||||||
import { useAuth } from '../../providers/Auth/index.js'
|
import { useAuth } from '../../providers/Auth/index.js'
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { ListQueryProvider } from '../../providers/ListQuery/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 { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { hoistQueryParamsToAnd } from '../../utilities/mergeListSearchAndWhere.js'
|
import { hoistQueryParamsToAnd } from '../../utilities/mergeListSearchAndWhere.js'
|
||||||
import { AnimateHeight } from '../AnimateHeight/index.js'
|
import { AnimateHeight } from '../AnimateHeight/index.js'
|
||||||
import { ColumnSelector } from '../ColumnSelector/index.js'
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
import { ColumnSelector } from '../ColumnSelector/index.js'
|
||||||
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
|
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
|
||||||
|
import { Popup, PopupList } from '../Popup/index.js'
|
||||||
import { RelationshipProvider } from '../Table/RelationshipProvider/index.js'
|
import { RelationshipProvider } from '../Table/RelationshipProvider/index.js'
|
||||||
import { TableColumnsProvider } from '../TableColumns/index.js'
|
import { TableColumnsProvider } from '../TableColumns/index.js'
|
||||||
import { DrawerLink } from './cells/DrawerLink/index.js'
|
import { DrawerLink } from './cells/DrawerLink/index.js'
|
||||||
@@ -37,7 +46,12 @@ type RelationshipTableComponentProps = {
|
|||||||
readonly initialData?: PaginatedDocs
|
readonly initialData?: PaginatedDocs
|
||||||
readonly initialDrawerData?: DocumentDrawerProps['initialData']
|
readonly initialDrawerData?: DocumentDrawerProps['initialData']
|
||||||
readonly Label?: React.ReactNode
|
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) => {
|
export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (props) => {
|
||||||
@@ -51,10 +65,11 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
initialData: initialDataFromProps,
|
initialData: initialDataFromProps,
|
||||||
initialDrawerData,
|
initialDrawerData,
|
||||||
Label,
|
Label,
|
||||||
|
parent,
|
||||||
relationTo,
|
relationTo,
|
||||||
} = props
|
} = props
|
||||||
const [Table, setTable] = useState<React.ReactNode>(null)
|
const [Table, setTable] = useState<React.ReactNode>(null)
|
||||||
const { getEntityConfig } = useConfig()
|
const { config, getEntityConfig } = useConfig()
|
||||||
|
|
||||||
const { permissions } = useAuth()
|
const { permissions } = useAuth()
|
||||||
|
|
||||||
@@ -86,6 +101,9 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
|
|
||||||
const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug: relationTo }))
|
const [collectionConfig] = useState(() => getEntityConfig({ collectionSlug: relationTo }))
|
||||||
|
|
||||||
|
const [selectedCollection, setSelectedCollection] = useState(
|
||||||
|
Array.isArray(relationTo) ? undefined : relationTo,
|
||||||
|
)
|
||||||
const [isLoadingTable, setIsLoadingTable] = useState(!disableTable)
|
const [isLoadingTable, setIsLoadingTable] = useState(!disableTable)
|
||||||
const [data, setData] = useState<PaginatedDocs>(initialData)
|
const [data, setData] = useState<PaginatedDocs>(initialData)
|
||||||
const [columnState, setColumnState] = useState<Column[]>()
|
const [columnState, setColumnState] = useState<Column[]>()
|
||||||
@@ -95,8 +113,8 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
const renderTable = useCallback(
|
const renderTable = useCallback(
|
||||||
async (docs?: PaginatedDocs['docs']) => {
|
async (docs?: PaginatedDocs['docs']) => {
|
||||||
const newQuery: ListQuery = {
|
const newQuery: ListQuery = {
|
||||||
limit: String(field.defaultLimit || collectionConfig.admin.pagination.defaultLimit),
|
limit: String(field?.defaultLimit || collectionConfig?.admin?.pagination?.defaultLimit),
|
||||||
sort: field.defaultSort || collectionConfig.defaultSort,
|
sort: field.defaultSort || collectionConfig?.defaultSort,
|
||||||
...(query || {}),
|
...(query || {}),
|
||||||
where: { ...(query?.where || {}) },
|
where: { ...(query?.where || {}) },
|
||||||
}
|
}
|
||||||
@@ -122,6 +140,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
docs,
|
docs,
|
||||||
enableRowSelections: false,
|
enableRowSelections: false,
|
||||||
|
parent,
|
||||||
query: newQuery,
|
query: newQuery,
|
||||||
renderRowTypes: true,
|
renderRowTypes: true,
|
||||||
tableAppearance: 'condensed',
|
tableAppearance: 'condensed',
|
||||||
@@ -136,12 +155,13 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
field.defaultLimit,
|
field.defaultLimit,
|
||||||
field.defaultSort,
|
field.defaultSort,
|
||||||
field.admin.defaultColumns,
|
field.admin.defaultColumns,
|
||||||
collectionConfig.admin.pagination.defaultLimit,
|
collectionConfig?.admin?.pagination?.defaultLimit,
|
||||||
collectionConfig.defaultSort,
|
collectionConfig?.defaultSort,
|
||||||
query,
|
query,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
getTableState,
|
getTableState,
|
||||||
relationTo,
|
relationTo,
|
||||||
|
parent,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,8 +175,9 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
handleTableRender(query, disableTable)
|
handleTableRender(query, disableTable)
|
||||||
}, [query, disableTable])
|
}, [query, disableTable])
|
||||||
|
|
||||||
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer, openDrawer }] = useDocumentDrawer({
|
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer, isDrawerOpen, openDrawer }] =
|
||||||
collectionSlug: relationTo,
|
useDocumentDrawer({
|
||||||
|
collectionSlug: selectedCollection,
|
||||||
})
|
})
|
||||||
|
|
||||||
const onDrawerSave = useCallback<DocumentDrawerProps['onSave']>(
|
const onDrawerSave = useCallback<DocumentDrawerProps['onSave']>(
|
||||||
@@ -174,12 +195,13 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
|
|
||||||
void renderTable(withNewOrUpdatedDoc)
|
void renderTable(withNewOrUpdatedDoc)
|
||||||
},
|
},
|
||||||
[data.docs, renderTable],
|
[data?.docs, renderTable],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onDrawerCreate = useCallback<DocumentDrawerProps['onSave']>(
|
const onDrawerCreate = useCallback<DocumentDrawerProps['onSave']>(
|
||||||
(args) => {
|
(args) => {
|
||||||
closeDrawer()
|
closeDrawer()
|
||||||
|
|
||||||
void onDrawerSave(args)
|
void onDrawerSave(args)
|
||||||
},
|
},
|
||||||
[closeDrawer, onDrawerSave],
|
[closeDrawer, onDrawerSave],
|
||||||
@@ -190,23 +212,80 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
const newDocs = data.docs.filter((doc) => doc.id !== args.id)
|
const newDocs = data.docs.filter((doc) => doc.id !== args.id)
|
||||||
void renderTable(newDocs)
|
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 (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<div className={`${baseClass}__header`}>
|
<div className={`${baseClass}__header`}>
|
||||||
{Label}
|
{Label}
|
||||||
<div className={`${baseClass}__actions`}>
|
<div className={`${baseClass}__actions`}>
|
||||||
{canCreate && (
|
{!Array.isArray(relationTo) && canCreate && (
|
||||||
<DocumentDrawerToggler className={`${baseClass}__add-new`}>
|
<DocumentDrawerToggler className={`${baseClass}__add-new`}>
|
||||||
{i18n.t('fields:addNew')}
|
{i18n.t('fields:addNew')}
|
||||||
</DocumentDrawerToggler>
|
</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
|
<Pill
|
||||||
aria-controls={`${baseClass}-columns`}
|
aria-controls={`${baseClass}-columns`}
|
||||||
aria-expanded={openColumnSelector}
|
aria-expanded={openColumnSelector}
|
||||||
@@ -226,11 +305,13 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
<p>{t('general:loading')}</p>
|
<p>{t('general:loading')}</p>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{data.docs && data.docs.length === 0 && (
|
{data?.docs && data.docs.length === 0 && (
|
||||||
<div className={`${baseClass}__no-results`}>
|
<div className={`${baseClass}__no-results`}>
|
||||||
<p>
|
<p>
|
||||||
{i18n.t('general:noResults', {
|
{i18n.t('general:noResults', {
|
||||||
label: getTranslation(collectionConfig?.labels?.plural, i18n),
|
label: Array.isArray(relationTo)
|
||||||
|
? i18n.t('general:documents')
|
||||||
|
: getTranslation(collectionConfig?.labels?.plural, i18n),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
{canCreate && (
|
{canCreate && (
|
||||||
@@ -242,7 +323,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data.docs && data.docs.length > 0 && (
|
{data?.docs && data.docs.length > 0 && (
|
||||||
<RelationshipProvider>
|
<RelationshipProvider>
|
||||||
<ListQueryProvider
|
<ListQueryProvider
|
||||||
data={data}
|
data={data}
|
||||||
@@ -253,7 +334,7 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
onQueryChange={setQuery}
|
onQueryChange={setQuery}
|
||||||
>
|
>
|
||||||
<TableColumnsProvider
|
<TableColumnsProvider
|
||||||
collectionSlug={relationTo}
|
collectionSlug={Array.isArray(relationTo) ? relationTo[0] : relationTo}
|
||||||
columnState={columnState}
|
columnState={columnState}
|
||||||
docs={data.docs}
|
docs={data.docs}
|
||||||
LinkedCellOverride={
|
LinkedCellOverride={
|
||||||
@@ -273,7 +354,9 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
id={`${baseClass}-columns`}
|
id={`${baseClass}-columns`}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__columns-inner`}>
|
<div className={`${baseClass}__columns-inner`}>
|
||||||
|
{collectionConfig && (
|
||||||
<ColumnSelector collectionSlug={collectionConfig.slug} />
|
<ColumnSelector collectionSlug={collectionConfig.slug} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AnimateHeight>
|
</AnimateHeight>
|
||||||
{Table}
|
{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 = {
|
type Props = {
|
||||||
readonly children: React.ReactNode
|
readonly children: React.ReactNode
|
||||||
readonly collectionSlug: string
|
readonly collectionSlug: string | string[]
|
||||||
readonly columnState: Column[]
|
readonly columnState: Column[]
|
||||||
readonly docs: any[]
|
readonly docs: any[]
|
||||||
readonly enableRowSelections?: boolean
|
readonly enableRowSelections?: boolean
|
||||||
@@ -68,7 +68,9 @@ export const TableColumnsProvider: React.FC<Props> = ({
|
|||||||
collectionSlug,
|
collectionSlug,
|
||||||
})
|
})
|
||||||
|
|
||||||
const prevCollection = React.useRef<SanitizedCollectionConfig['slug']>(collectionSlug)
|
const prevCollection = React.useRef<SanitizedCollectionConfig['slug']>(
|
||||||
|
Array.isArray(collectionSlug) ? collectionSlug[0] : collectionSlug,
|
||||||
|
)
|
||||||
const { getPreference } = usePreferences()
|
const { getPreference } = usePreferences()
|
||||||
|
|
||||||
const [tableColumns, setTableColumns] = React.useState(columnState)
|
const [tableColumns, setTableColumns] = React.useState(columnState)
|
||||||
@@ -232,14 +234,15 @@ export const TableColumnsProvider: React.FC<Props> = ({
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const sync = async () => {
|
const sync = async () => {
|
||||||
const collectionHasChanged = prevCollection.current !== collectionSlug
|
const defaultCollection = Array.isArray(collectionSlug) ? collectionSlug[0] : collectionSlug
|
||||||
|
const collectionHasChanged = prevCollection.current !== defaultCollection
|
||||||
|
|
||||||
if (collectionHasChanged || !listPreferences) {
|
if (collectionHasChanged || !listPreferences) {
|
||||||
const currentPreferences = await getPreference<{
|
const currentPreferences = await getPreference<{
|
||||||
columns: ListPreferences['columns']
|
columns: ListPreferences['columns']
|
||||||
}>(preferenceKey)
|
}>(preferenceKey)
|
||||||
|
|
||||||
prevCollection.current = collectionSlug
|
prevCollection.current = defaultCollection
|
||||||
|
|
||||||
if (currentPreferences?.columns) {
|
if (currentPreferences?.columns) {
|
||||||
// setTableColumns()
|
// setTableColumns()
|
||||||
|
|||||||
@@ -160,7 +160,9 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const where = {
|
const where = Array.isArray(collection)
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
[on]: {
|
[on]: {
|
||||||
equals: value,
|
equals: value,
|
||||||
},
|
},
|
||||||
@@ -173,10 +175,12 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return where
|
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 initialDrawerData = useMemo(() => {
|
||||||
const relatedCollection = getEntityConfig({ collectionSlug: field.collection })
|
const relatedCollection = getEntityConfig({
|
||||||
|
collectionSlug: Array.isArray(field.collection) ? field.collection[0] : field.collection,
|
||||||
|
})
|
||||||
|
|
||||||
return getInitialDrawerData({
|
return getInitialDrawerData({
|
||||||
collectionSlug: docConfig?.slug,
|
collectionSlug: docConfig?.slug,
|
||||||
@@ -216,6 +220,15 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
}
|
}
|
||||||
|
parent={
|
||||||
|
Array.isArray(collection)
|
||||||
|
? {
|
||||||
|
id: docID,
|
||||||
|
collectionSlug: docConfig.slug,
|
||||||
|
joinPath: path,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
relationTo={collection}
|
relationTo={collection}
|
||||||
/>
|
/>
|
||||||
<RenderCustomComponent
|
<RenderCustomComponent
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import type {
|
|||||||
ListPreferences,
|
ListPreferences,
|
||||||
PaginatedDocs,
|
PaginatedDocs,
|
||||||
SanitizedCollectionConfig,
|
SanitizedCollectionConfig,
|
||||||
|
Where,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { formatErrors } from 'payload'
|
import { APIError, formatErrors } from 'payload'
|
||||||
import { isNumber } from 'payload/shared'
|
import { isNumber } from 'payload/shared'
|
||||||
|
|
||||||
import { getClientConfig } from './getClientConfig.js'
|
import { getClientConfig } from './getClientConfig.js'
|
||||||
@@ -73,6 +74,7 @@ export const buildTableState = async (
|
|||||||
columns,
|
columns,
|
||||||
docs: docsFromArgs,
|
docs: docsFromArgs,
|
||||||
enableRowSelections,
|
enableRowSelections,
|
||||||
|
parent,
|
||||||
query,
|
query,
|
||||||
renderRowTypes,
|
renderRowTypes,
|
||||||
req,
|
req,
|
||||||
@@ -128,15 +130,19 @@ export const buildTableState = async (
|
|||||||
let collectionConfig: SanitizedCollectionConfig
|
let collectionConfig: SanitizedCollectionConfig
|
||||||
let clientCollectionConfig: ClientCollectionConfig
|
let clientCollectionConfig: ClientCollectionConfig
|
||||||
|
|
||||||
|
if (!Array.isArray(collectionSlug)) {
|
||||||
if (req.payload.collections[collectionSlug]) {
|
if (req.payload.collections[collectionSlug]) {
|
||||||
collectionConfig = req.payload.collections[collectionSlug].config
|
collectionConfig = req.payload.collections[collectionSlug].config
|
||||||
clientCollectionConfig = clientConfig.collections.find(
|
clientCollectionConfig = clientConfig.collections.find(
|
||||||
(collection) => collection.slug === collectionSlug,
|
(collection) => collection.slug === collectionSlug,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const listPreferences = await upsertPreferences<ListPreferences>({
|
const listPreferences = await upsertPreferences<ListPreferences>({
|
||||||
key: `${collectionSlug}-list`,
|
key: Array.isArray(collectionSlug)
|
||||||
|
? `${parent.collectionSlug}-${parent.joinPath}`
|
||||||
|
: `${collectionSlug}-list`,
|
||||||
req,
|
req,
|
||||||
value: {
|
value: {
|
||||||
columns,
|
columns,
|
||||||
@@ -151,6 +157,57 @@ export const buildTableState = async (
|
|||||||
// lookup docs, if desired, i.e. within `join` field which initialize with `depth: 0`
|
// lookup docs, if desired, i.e. within `join` field which initialize with `depth: 0`
|
||||||
|
|
||||||
if (!docs || query) {
|
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({
|
data = await payload.find({
|
||||||
collection: collectionSlug,
|
collection: collectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
@@ -162,14 +219,16 @@ export const buildTableState = async (
|
|||||||
user: req.user,
|
user: req.user,
|
||||||
where: query?.where,
|
where: query?.where,
|
||||||
})
|
})
|
||||||
|
|
||||||
docs = data.docs
|
docs = data.docs
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { columnState, Table } = renderTable({
|
const { columnState, Table } = renderTable({
|
||||||
clientCollectionConfig,
|
clientCollectionConfig,
|
||||||
|
clientConfig,
|
||||||
collectionConfig,
|
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,
|
columns,
|
||||||
docs,
|
docs,
|
||||||
enableRowSelections,
|
enableRowSelections,
|
||||||
@@ -177,10 +236,16 @@ export const buildTableState = async (
|
|||||||
payload,
|
payload,
|
||||||
renderRowTypes,
|
renderRowTypes,
|
||||||
tableAppearance,
|
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 {
|
return {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ClientCollectionConfig,
|
ClientCollectionConfig,
|
||||||
|
ClientConfig,
|
||||||
|
ClientField,
|
||||||
CollectionConfig,
|
CollectionConfig,
|
||||||
Field,
|
Field,
|
||||||
ImportMap,
|
ImportMap,
|
||||||
@@ -10,13 +12,14 @@ import type {
|
|||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
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
|
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||||
import type { Column } from '../exports/client/index.js'
|
import type { Column } from '../exports/client/index.js'
|
||||||
|
|
||||||
import { RenderServerComponent } from '../elements/RenderServerComponent/index.js'
|
import { RenderServerComponent } from '../elements/RenderServerComponent/index.js'
|
||||||
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
|
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
|
||||||
|
import { buildPolymorphicColumnState } from '../elements/TableColumns/buildPolymorphicColumnState.js'
|
||||||
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
||||||
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
||||||
|
|
||||||
@@ -50,7 +53,9 @@ export const renderFilters = (
|
|||||||
|
|
||||||
export const renderTable = ({
|
export const renderTable = ({
|
||||||
clientCollectionConfig,
|
clientCollectionConfig,
|
||||||
|
clientConfig,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
|
collections,
|
||||||
columnPreferences,
|
columnPreferences,
|
||||||
columns: columnsFromArgs,
|
columns: columnsFromArgs,
|
||||||
customCellProps,
|
customCellProps,
|
||||||
@@ -62,8 +67,10 @@ export const renderTable = ({
|
|||||||
tableAppearance,
|
tableAppearance,
|
||||||
useAsTitle,
|
useAsTitle,
|
||||||
}: {
|
}: {
|
||||||
clientCollectionConfig: ClientCollectionConfig
|
clientCollectionConfig?: ClientCollectionConfig
|
||||||
collectionConfig: SanitizedCollectionConfig
|
clientConfig?: ClientConfig
|
||||||
|
collectionConfig?: SanitizedCollectionConfig
|
||||||
|
collections?: string[]
|
||||||
columnPreferences: ListPreferences['columns']
|
columnPreferences: ListPreferences['columns']
|
||||||
columns?: ListPreferences['columns']
|
columns?: ListPreferences['columns']
|
||||||
customCellProps?: Record<string, any>
|
customCellProps?: Record<string, any>
|
||||||
@@ -80,6 +87,46 @@ export const renderTable = ({
|
|||||||
Table: React.ReactNode
|
Table: React.ReactNode
|
||||||
} => {
|
} => {
|
||||||
// Ensure that columns passed as args comply with the field config, i.e. `hidden`, `disableListColumn`, etc.
|
// 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
|
const columns = columnsFromArgs
|
||||||
? columnsFromArgs?.filter((column) =>
|
? columnsFromArgs?.filter((column) =>
|
||||||
flattenTopLevelFields(clientCollectionConfig.fields, true)?.some(
|
flattenTopLevelFields(clientCollectionConfig.fields, true)?.some(
|
||||||
@@ -92,7 +139,7 @@ export const renderTable = ({
|
|||||||
clientCollectionConfig?.admin?.defaultColumns,
|
clientCollectionConfig?.admin?.defaultColumns,
|
||||||
)
|
)
|
||||||
|
|
||||||
const columnState = buildColumnState({
|
columnState = buildColumnState({
|
||||||
clientCollectionConfig,
|
clientCollectionConfig,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
columnPreferences,
|
columnPreferences,
|
||||||
@@ -105,6 +152,7 @@ export const renderTable = ({
|
|||||||
payload,
|
payload,
|
||||||
useAsTitle,
|
useAsTitle,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const columnsToUse = [...columnState]
|
const columnsToUse = [...columnState]
|
||||||
|
|
||||||
@@ -119,8 +167,15 @@ export const renderTable = ({
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
Heading: i18n.t('version:type'),
|
Heading: i18n.t('version:type'),
|
||||||
renderedCells: docs.map((_, i) => (
|
renderedCells: docs.map((doc, i) => (
|
||||||
<Pill key={i}>{getTranslation(clientCollectionConfig.labels.singular, i18n)}</Pill>
|
<Pill key={i}>
|
||||||
|
{getTranslation(
|
||||||
|
collections
|
||||||
|
? payload.collections[doc.relationTo].config.labels.singular
|
||||||
|
: clientCollectionConfig.labels.singular,
|
||||||
|
i18n,
|
||||||
|
)}
|
||||||
|
</Pill>
|
||||||
)),
|
)),
|
||||||
} as Column)
|
} 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: {
|
localization: {
|
||||||
locales: [
|
locales: [
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ const { beforeAll, beforeEach, describe } = test
|
|||||||
describe('Join Field', () => {
|
describe('Join Field', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
let categoriesURL: AdminUrlUtil
|
let categoriesURL: AdminUrlUtil
|
||||||
|
let foldersURL: AdminUrlUtil
|
||||||
let uploadsURL: AdminUrlUtil
|
let uploadsURL: AdminUrlUtil
|
||||||
let categoriesJoinRestrictedURL: AdminUrlUtil
|
let categoriesJoinRestrictedURL: AdminUrlUtil
|
||||||
let categoryID: number | string
|
let categoryID: number | string
|
||||||
|
let rootFolderID: number | string
|
||||||
|
|
||||||
beforeAll(async ({ browser }, testInfo) => {
|
beforeAll(async ({ browser }, testInfo) => {
|
||||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||||
@@ -50,6 +52,7 @@ describe('Join Field', () => {
|
|||||||
categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug)
|
categoriesURL = new AdminUrlUtil(serverURL, categoriesSlug)
|
||||||
uploadsURL = new AdminUrlUtil(serverURL, uploadsSlug)
|
uploadsURL = new AdminUrlUtil(serverURL, uploadsSlug)
|
||||||
categoriesJoinRestrictedURL = new AdminUrlUtil(serverURL, categoriesJoinRestrictedSlug)
|
categoriesJoinRestrictedURL = new AdminUrlUtil(serverURL, categoriesJoinRestrictedSlug)
|
||||||
|
foldersURL = new AdminUrlUtil(serverURL, 'folders')
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -86,6 +89,9 @@ describe('Join Field', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
;({ id: categoryID } = docs[0])
|
;({ 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 () => {
|
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')
|
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 () => {
|
test('should render create-first-user with when users collection has a join field and hide it', async () => {
|
||||||
await payload.delete({ collection: 'users', where: {} })
|
await payload.delete({ collection: 'users', where: {} })
|
||||||
const url = new AdminUrlUtil(serverURL, 'users')
|
const url = new AdminUrlUtil(serverURL, 'users')
|
||||||
|
|||||||
@@ -1153,6 +1153,123 @@ describe('Joins Field', () => {
|
|||||||
|
|
||||||
expect(joinedDoc2.id).toBe(depthJoin_3.id)
|
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']) {
|
async function createPost(overrides?: Partial<Post>, locale?: Config['locale']) {
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ export interface Config {
|
|||||||
'depth-joins-1': DepthJoins1;
|
'depth-joins-1': DepthJoins1;
|
||||||
'depth-joins-2': DepthJoins2;
|
'depth-joins-2': DepthJoins2;
|
||||||
'depth-joins-3': DepthJoins3;
|
'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-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
@@ -135,6 +141,12 @@ export interface Config {
|
|||||||
'depth-joins-2': {
|
'depth-joins-2': {
|
||||||
joins: 'depth-joins-1';
|
joins: 'depth-joins-1';
|
||||||
};
|
};
|
||||||
|
'multiple-collections-parents': {
|
||||||
|
children: 'multiple-collections-1' | 'multiple-collections-2';
|
||||||
|
};
|
||||||
|
folders: {
|
||||||
|
children: 'folders' | 'example-pages' | 'example-posts';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
collectionsSelect: {
|
collectionsSelect: {
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
@@ -155,6 +167,12 @@ export interface Config {
|
|||||||
'depth-joins-1': DepthJoins1Select<false> | DepthJoins1Select<true>;
|
'depth-joins-1': DepthJoins1Select<false> | DepthJoins1Select<true>;
|
||||||
'depth-joins-2': DepthJoins2Select<false> | DepthJoins2Select<true>;
|
'depth-joins-2': DepthJoins2Select<false> | DepthJoins2Select<true>;
|
||||||
'depth-joins-3': DepthJoins3Select<false> | DepthJoins3Select<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-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
@@ -581,6 +599,108 @@ export interface DepthJoins3 {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
@@ -659,6 +779,30 @@ export interface PayloadLockedDocument {
|
|||||||
| ({
|
| ({
|
||||||
relationTo: 'depth-joins-3';
|
relationTo: 'depth-joins-3';
|
||||||
value: string | DepthJoins3;
|
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);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
@@ -958,6 +1102,70 @@ export interface DepthJoins3Select<T extends boolean = true> {
|
|||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents_select".
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
|||||||
@@ -146,6 +146,74 @@ export const seed = async (_payload: Payload) => {
|
|||||||
category: restrictedCategory.id,
|
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) {
|
export async function clearAndSeedEverything(_payload: Payload) {
|
||||||
|
|||||||
@@ -16,21 +16,13 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": [
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
"DOM",
|
|
||||||
"DOM.Iterable",
|
|
||||||
"ES2022"
|
|
||||||
],
|
|
||||||
"outDir": "${configDir}/dist",
|
"outDir": "${configDir}/dist",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"types": [
|
"types": ["jest", "node", "@types/jest"],
|
||||||
"jest",
|
|
||||||
"node",
|
|
||||||
"@types/jest"
|
|
||||||
],
|
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -51,33 +43,19 @@
|
|||||||
"@payloadcms/richtext-lexical/client": [
|
"@payloadcms/richtext-lexical/client": [
|
||||||
"./packages/richtext-lexical/src/exports/client/index.ts"
|
"./packages/richtext-lexical/src/exports/client/index.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/richtext-lexical/rsc": [
|
"@payloadcms/richtext-lexical/rsc": ["./packages/richtext-lexical/src/exports/server/rsc.ts"],
|
||||||
"./packages/richtext-lexical/src/exports/server/rsc.ts"
|
"@payloadcms/richtext-slate/rsc": ["./packages/richtext-slate/src/exports/server/rsc.ts"],
|
||||||
],
|
|
||||||
"@payloadcms/richtext-slate/rsc": [
|
|
||||||
"./packages/richtext-slate/src/exports/server/rsc.ts"
|
|
||||||
],
|
|
||||||
"@payloadcms/richtext-slate/client": [
|
"@payloadcms/richtext-slate/client": [
|
||||||
"./packages/richtext-slate/src/exports/client/index.ts"
|
"./packages/richtext-slate/src/exports/client/index.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/plugin-seo/client": [
|
"@payloadcms/plugin-seo/client": ["./packages/plugin-seo/src/exports/client.ts"],
|
||||||
"./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-sentry/client": [
|
"@payloadcms/plugin-search/client": ["./packages/plugin-search/src/exports/client.ts"],
|
||||||
"./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": [
|
"@payloadcms/plugin-form-builder/client": [
|
||||||
"./packages/plugin-form-builder/src/exports/client.ts"
|
"./packages/plugin-form-builder/src/exports/client.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/plugin-multi-tenant/rsc": [
|
"@payloadcms/plugin-multi-tenant/rsc": ["./packages/plugin-multi-tenant/src/exports/rsc.ts"],
|
||||||
"./packages/plugin-multi-tenant/src/exports/rsc.ts"
|
|
||||||
],
|
|
||||||
"@payloadcms/plugin-multi-tenant/utilities": [
|
"@payloadcms/plugin-multi-tenant/utilities": [
|
||||||
"./packages/plugin-multi-tenant/src/exports/utilities.ts"
|
"./packages/plugin-multi-tenant/src/exports/utilities.ts"
|
||||||
],
|
],
|
||||||
@@ -87,21 +65,10 @@
|
|||||||
"@payloadcms/plugin-multi-tenant/client": [
|
"@payloadcms/plugin-multi-tenant/client": [
|
||||||
"./packages/plugin-multi-tenant/src/exports/client.ts"
|
"./packages/plugin-multi-tenant/src/exports/client.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/plugin-multi-tenant": [
|
"@payloadcms/plugin-multi-tenant": ["./packages/plugin-multi-tenant/src/index.ts"],
|
||||||
"./packages/plugin-multi-tenant/src/index.ts"
|
"@payloadcms/next": ["./packages/next/src/exports/*"]
|
||||||
],
|
|
||||||
"@payloadcms/next": [
|
|
||||||
"./packages/next/src/exports/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["${configDir}/src"],
|
||||||
"${configDir}/src"
|
"exclude": ["${configDir}/dist", "${configDir}/build", "${configDir}/temp", "**/*.spec.ts"]
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"${configDir}/dist",
|
|
||||||
"${configDir}/build",
|
|
||||||
"${configDir}/temp",
|
|
||||||
"**/*.spec.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user