Compare commits
10 Commits
main
...
feat/list-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bb2202b1 | ||
|
|
4d89a2b747 | ||
|
|
e62c8a5661 | ||
|
|
19fea26447 | ||
|
|
38c346b4c7 | ||
|
|
142228e70b | ||
|
|
ad892a1da4 | ||
|
|
414e5d9363 | ||
|
|
195accdc00 | ||
|
|
7bb5321a95 |
@@ -85,7 +85,7 @@ The following options are available:
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks. [More details](../queries/select). |
|
||||
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
@@ -141,6 +141,7 @@ The following options are available:
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
|
||||
| `enableListViewSelectAPI` | Performance opt-in. When `true`, uses the Select API in the List View to query only the active columns as opposed to entire documents. [More details](#enable-list-view-select-api). |
|
||||
| `pagination` | Set pagination-specific options for this Collection in the List View. [More details](#pagination). |
|
||||
| `baseFilter` | Defines a default base filter which will be applied to the List View (along with any other filters applied by the user) and internal links in Lexical Editor, |
|
||||
|
||||
@@ -272,6 +273,66 @@ export const Posts: CollectionConfig = {
|
||||
these fields so your admin queries can remain performant.
|
||||
</Banner>
|
||||
|
||||
## Enable List View Select API
|
||||
|
||||
When `true`, the List View will use the [Select API](../queries/select) to query only the _active_ columns as opposed to entire documents. This can greatly improve performance, especially for collections with large documents or many fields.
|
||||
|
||||
To enable this, set `enableListViewSelectAPI: true` in your Collection Config:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// ...
|
||||
// highlight-start
|
||||
enableListViewSelectAPI: true,
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Note:** The `enableListViewSelectAPI` property is labeled as experimental,
|
||||
as it will likely become the default behavior in v4 and be deprecated.
|
||||
</Banner>
|
||||
|
||||
Enabling this feature may cause unexpected behavior in some cases, however, such as when using hooks that rely on the full document data.
|
||||
|
||||
For example, if your component relies on a "title" field, this field will no longer be populated if the column is inactive:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
hooks: {
|
||||
afterRead: [
|
||||
({ doc }) => doc.title, // The `title` field will no longer be populated by default, unless the column is active
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
To ensure title is always present, you will need to add that field to the [`forceSelect`](../queries/select) property in your Collection Config:
|
||||
|
||||
```ts
|
||||
export const Posts: CollectionConfig = {
|
||||
// ...
|
||||
forceSelect: {
|
||||
title: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## GraphQL
|
||||
|
||||
You can completely disable GraphQL for this collection by passing `graphQL: false` to your collection config. This will completely disable all queries, mutations, and types from appearing in your GraphQL schema.
|
||||
|
||||
@@ -84,7 +84,7 @@ The following options are available:
|
||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Global. |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#global-config). |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks. [More details](../queries/select). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export type Args = {
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
*
|
||||
* @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle.
|
||||
*/
|
||||
schemaName?: string
|
||||
|
||||
@@ -72,6 +72,7 @@ export type Args = {
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
*
|
||||
* @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle.
|
||||
*/
|
||||
schemaName?: string
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {
|
||||
ClientCollectionConfig,
|
||||
ClientConfig,
|
||||
Column,
|
||||
ListQuery,
|
||||
PaginatedDocs,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
SelectType,
|
||||
ViewTypes,
|
||||
Where,
|
||||
} from 'payload'
|
||||
@@ -14,6 +16,7 @@ import { formatDate } from '@payloadcms/ui/shared'
|
||||
import { flattenAllFields } from 'payload'
|
||||
|
||||
export const handleGroupBy = async ({
|
||||
clientCollectionConfig,
|
||||
clientConfig,
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
@@ -23,11 +26,13 @@ export const handleGroupBy = async ({
|
||||
enableRowSelections,
|
||||
query,
|
||||
req,
|
||||
select,
|
||||
trash = false,
|
||||
user,
|
||||
viewType,
|
||||
where: whereWithMergedSearch,
|
||||
}: {
|
||||
clientCollectionConfig: ClientCollectionConfig
|
||||
clientConfig: ClientConfig
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
collectionSlug: string
|
||||
@@ -37,6 +42,7 @@ export const handleGroupBy = async ({
|
||||
enableRowSelections?: boolean
|
||||
query?: ListQuery
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
trash?: boolean
|
||||
user: any
|
||||
viewType?: ViewTypes
|
||||
@@ -50,7 +56,6 @@ export const handleGroupBy = async ({
|
||||
let columnState: Column[]
|
||||
|
||||
const dataByGroup: Record<string, PaginatedDocs> = {}
|
||||
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
|
||||
|
||||
// NOTE: is there a faster/better way to do this?
|
||||
const flattenedFields = flattenAllFields({ fields: collectionConfig.fields })
|
||||
@@ -132,6 +137,7 @@ export const handleGroupBy = async ({
|
||||
req,
|
||||
// Note: if we wanted to enable table-by-table sorting, we could use this:
|
||||
// sort: query?.queryByGroup?.[valueOrRelationshipID]?.sort,
|
||||
select,
|
||||
sort: query?.sort,
|
||||
trash,
|
||||
user,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
|
||||
import { getColumns, renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import {
|
||||
type AdminViewServerProps,
|
||||
@@ -28,6 +28,7 @@ import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { handleGroupBy } from './handleGroupBy.js'
|
||||
import { renderListViewSlots } from './renderListViewSlots.js'
|
||||
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
|
||||
import { transformColumnsToSelect } from './transformColumnsToSelect.js'
|
||||
|
||||
type RenderListViewArgs = {
|
||||
customCellProps?: Record<string, any>
|
||||
@@ -208,18 +209,32 @@ export const renderListView = async (
|
||||
totalPages: 0,
|
||||
}
|
||||
|
||||
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
|
||||
|
||||
const columns = getColumns({
|
||||
collectionConfig: clientCollectionConfig,
|
||||
columns: collectionPreferences?.columns,
|
||||
i18n,
|
||||
})
|
||||
|
||||
const select = collectionConfig.admin.enableListViewSelectAPI
|
||||
? transformColumnsToSelect(columns)
|
||||
: undefined
|
||||
|
||||
try {
|
||||
if (collectionConfig.admin.groupBy && query.groupBy) {
|
||||
;({ columnState, data, Table } = await handleGroupBy({
|
||||
clientCollectionConfig,
|
||||
clientConfig,
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
columns: collectionPreferences?.columns,
|
||||
columns,
|
||||
customCellProps,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
query,
|
||||
req,
|
||||
select,
|
||||
trash,
|
||||
user,
|
||||
viewType,
|
||||
@@ -237,15 +252,16 @@ export const renderListView = async (
|
||||
overrideAccess: false,
|
||||
page: query?.page ? Number(query.page) : undefined,
|
||||
req,
|
||||
select,
|
||||
sort: query?.sort,
|
||||
trash,
|
||||
user,
|
||||
where: whereWithMergedSearch,
|
||||
})
|
||||
;({ columnState, Table } = renderTable({
|
||||
clientCollectionConfig: clientConfig.collections.find((c) => c.slug === collectionSlug),
|
||||
clientCollectionConfig,
|
||||
collectionConfig,
|
||||
columns: collectionPreferences?.columns,
|
||||
columns,
|
||||
customCellProps,
|
||||
data,
|
||||
drawerSlug,
|
||||
|
||||
9
packages/next/src/views/List/transformColumnsToSelect.ts
Normal file
9
packages/next/src/views/List/transformColumnsToSelect.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { ColumnPreference, SelectType } from 'payload'
|
||||
|
||||
export const transformColumnsToSelect = (columns: ColumnPreference[]): SelectType =>
|
||||
columns.reduce((acc, column) => {
|
||||
if (column.active) {
|
||||
acc[column.accessor] = true
|
||||
}
|
||||
return acc
|
||||
}, {} as SelectType)
|
||||
@@ -58,9 +58,10 @@ export type FieldState = {
|
||||
filterOptions?: FilterOptionsResult
|
||||
initialValue?: unknown
|
||||
/**
|
||||
* @experimental - Note: this property is experimental and may change in the future. Use at your own discretion.
|
||||
* Every time a field is changed locally, this flag is set to true. Prevents form state from server from overwriting local changes.
|
||||
* After merging server form state, this flag is reset.
|
||||
*
|
||||
* @experimental This property is experimental and may change in the future. Use at your own discretion.
|
||||
*/
|
||||
isModified?: boolean
|
||||
/**
|
||||
|
||||
@@ -383,6 +383,15 @@ export type CollectionAdminOptions = {
|
||||
* @default false
|
||||
*/
|
||||
disableCopyToLocale?: boolean
|
||||
/**
|
||||
* Performance opt-in. If true, will use the [Select API](https://payloadcms.com/docs/queries/select) when
|
||||
* loading the list view to query only the active columns, as opposed to the entire documents.
|
||||
* If your cells require specific fields that may be unselected, such as within hooks, etc.,
|
||||
* use `forceSelect` in conjunction with this property.
|
||||
*
|
||||
* @experimental This is an experimental feature and may change in the future. Use at your own discretion.
|
||||
*/
|
||||
enableListViewSelectAPI?: boolean
|
||||
enableRichTextLink?: boolean
|
||||
enableRichTextRelationship?: boolean
|
||||
/**
|
||||
@@ -393,10 +402,10 @@ export type CollectionAdminOptions = {
|
||||
*/
|
||||
group?: false | Record<string, string> | string
|
||||
/**
|
||||
* @experimental This option is currently in beta and may change in future releases and/or contain bugs.
|
||||
* Use at your own risk.
|
||||
* @description Enable grouping by a field in the list view.
|
||||
* Uses `payload.findDistinct` under the hood to populate the group-by options.
|
||||
*
|
||||
* @experimental This option is currently in beta and may change in future releases. Use at your own discretion.
|
||||
*/
|
||||
groupBy?: boolean
|
||||
/**
|
||||
|
||||
@@ -948,9 +948,10 @@ export type Config = {
|
||||
*/
|
||||
timezones?: TimezonesConfig
|
||||
/**
|
||||
* @experimental
|
||||
* Configure toast message behavior and appearance in the admin panel.
|
||||
* Currently using [Sonner](https://sonner.emilkowal.ski) for toast notifications.
|
||||
*
|
||||
* @experimental This property is experimental and may change in future releases. Use at your own discretion.
|
||||
*/
|
||||
toast?: {
|
||||
/**
|
||||
@@ -1057,7 +1058,8 @@ export type Config = {
|
||||
experimental?: ExperimentalConfig
|
||||
/**
|
||||
* Options for folder view within the admin panel
|
||||
* @experimental this feature may change in minor versions until it is fully stable
|
||||
*
|
||||
* @experimental This feature may change in minor versions until it is fully stable
|
||||
*/
|
||||
folders?: false | RootFoldersConfiguration
|
||||
/**
|
||||
|
||||
@@ -69,7 +69,7 @@ type FlattenFieldsOptions = {
|
||||
* @param options - Options to control the flattening behavior
|
||||
*/
|
||||
export function flattenTopLevelFields<TField extends ClientField | Field>(
|
||||
fields: TField[],
|
||||
fields: TField[] = [],
|
||||
options?: boolean | FlattenFieldsOptions,
|
||||
): FlattenedField<TField>[] {
|
||||
const normalizedOptions: FlattenFieldsOptions =
|
||||
|
||||
@@ -21,8 +21,9 @@ export type DocumentDrawerContextProps = {
|
||||
readonly onSave?: (args: {
|
||||
collectionConfig?: ClientCollectionConfig
|
||||
/**
|
||||
* @experimental - Note: this property is experimental and may change in the future. Use at your own discretion.
|
||||
* If you want to pass additional data to the onSuccess callback, you can use this context object.
|
||||
*
|
||||
* @experimental This property is experimental and may change in the future. Use at your own discretion.
|
||||
*/
|
||||
context?: Record<string, unknown>
|
||||
doc: TypeWithID
|
||||
|
||||
@@ -6,6 +6,7 @@ export { getHTMLDiffComponents } from '../../elements/HTMLDiff/index.js'
|
||||
export { File } from '../../graphics/File/index.js'
|
||||
export { CheckIcon } from '../../icons/Check/index.js'
|
||||
export { copyDataFromLocaleHandler } from '../../utilities/copyDataFromLocale.js'
|
||||
export { getColumns } from '../../utilities/getColumns.js'
|
||||
export { getFolderResultsComponentAndData } from '../../utilities/getFolderResultsComponentAndData.js'
|
||||
export { renderFilters, renderTable } from '../../utilities/renderTable.js'
|
||||
export { resolveFilterOptions } from '../../utilities/resolveFilterOptions.js'
|
||||
|
||||
@@ -88,8 +88,9 @@ export type SubmitOptions<C = Record<string, unknown>> = {
|
||||
acceptValues?: AcceptValues
|
||||
action?: string
|
||||
/**
|
||||
* @experimental - Note: this property is experimental and may change in the future. Use at your own discretion.
|
||||
* If you want to pass additional data to the onSuccess callback, you can use this context object.
|
||||
*
|
||||
* @experimental This property is experimental and may change in the future.
|
||||
*/
|
||||
context?: C
|
||||
/**
|
||||
@@ -117,8 +118,9 @@ export type Submit = <T extends Response, C extends Record<string, unknown>>(
|
||||
options?: SubmitOptions<C>,
|
||||
e?: React.FormEvent<HTMLFormElement>,
|
||||
) => Promise</**
|
||||
* @experimental - Note: the `{ res: ... }` return type is experimental and may change in the future. Use at your own discretion.
|
||||
* Returns the form state and the response from the server.
|
||||
*
|
||||
* @experimental - Note: the `{ res: ... }` return type is experimental and may change in the future. Use at your own discretion.
|
||||
*/
|
||||
{ formState?: FormState; res: T } | void>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { APIError, formatErrors } from 'payload'
|
||||
import { isNumber } from 'payload/shared'
|
||||
|
||||
import { getClientConfig } from './getClientConfig.js'
|
||||
import { getColumns } from './getColumns.js'
|
||||
import { renderFilters, renderTable } from './renderTable.js'
|
||||
import { upsertPreferences } from './upsertPreferences.js'
|
||||
|
||||
@@ -73,7 +74,7 @@ const buildTableState = async (
|
||||
): Promise<BuildTableStateSuccessResult> => {
|
||||
const {
|
||||
collectionSlug,
|
||||
columns,
|
||||
columns: columnsFromArgs,
|
||||
data: dataFromArgs,
|
||||
enableRowSelections,
|
||||
orderableFieldName,
|
||||
@@ -148,7 +149,7 @@ const buildTableState = async (
|
||||
: `collection-${collectionSlug}`,
|
||||
req,
|
||||
value: {
|
||||
columns,
|
||||
columns: columnsFromArgs,
|
||||
limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
|
||||
sort: query?.sort as string,
|
||||
},
|
||||
@@ -229,7 +230,11 @@ const buildTableState = async (
|
||||
clientConfig,
|
||||
collectionConfig,
|
||||
collections: Array.isArray(collectionSlug) ? collectionSlug : undefined,
|
||||
columns,
|
||||
columns: getColumns({
|
||||
collectionConfig: clientCollectionConfig,
|
||||
columns: columnsFromArgs,
|
||||
i18n: req.i18n,
|
||||
}),
|
||||
data,
|
||||
enableRowSelections,
|
||||
i18n: req.i18n,
|
||||
|
||||
36
packages/ui/src/utilities/getColumns.ts
Normal file
36
packages/ui/src/utilities/getColumns.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientCollectionConfig, ColumnPreference } from 'payload'
|
||||
|
||||
import { flattenTopLevelFields } from 'payload'
|
||||
|
||||
import { filterFields } from '../providers/TableColumns/buildColumnState/filterFields.js'
|
||||
import { getInitialColumns } from '../providers/TableColumns/getInitialColumns.js'
|
||||
|
||||
export const getColumns = ({
|
||||
collectionConfig,
|
||||
columns,
|
||||
i18n,
|
||||
isPolymorphic,
|
||||
}: {
|
||||
collectionConfig?: ClientCollectionConfig
|
||||
columns: ColumnPreference[]
|
||||
i18n: I18nClient
|
||||
isPolymorphic?: boolean
|
||||
}) =>
|
||||
columns
|
||||
? columns?.filter((column) =>
|
||||
flattenTopLevelFields(collectionConfig?.fields, {
|
||||
i18n,
|
||||
keepPresentationalFields: true,
|
||||
moveSubFieldsToTop: true,
|
||||
})?.some((field) => {
|
||||
const accessor =
|
||||
'accessor' in field ? field.accessor : 'name' in field ? field.name : undefined
|
||||
return accessor === column.accessor
|
||||
}),
|
||||
)
|
||||
: getInitialColumns(
|
||||
isPolymorphic ? collectionConfig?.fields : filterFields(collectionConfig?.fields),
|
||||
collectionConfig.admin?.useAsTitle,
|
||||
isPolymorphic ? [] : collectionConfig?.admin?.defaultColumns,
|
||||
)
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
ClientConfig,
|
||||
ClientField,
|
||||
CollectionConfig,
|
||||
CollectionPreferences,
|
||||
Column,
|
||||
ColumnPreference,
|
||||
Field,
|
||||
@@ -16,7 +15,7 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
||||
import { fieldAffectsData, fieldIsHiddenOrDisabled, flattenTopLevelFields } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import type { BuildColumnStateArgs } from '../providers/TableColumns/buildColumnState/index.js'
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
} from '../exports/client/index.js'
|
||||
import { filterFields } from '../providers/TableColumns/buildColumnState/filterFields.js'
|
||||
import { buildColumnState } from '../providers/TableColumns/buildColumnState/index.js'
|
||||
import { getInitialColumns } from '../providers/TableColumns/getInitialColumns.js'
|
||||
|
||||
export const renderFilters = (
|
||||
fields: Field[],
|
||||
@@ -69,7 +67,7 @@ export const renderTable = ({
|
||||
clientConfig,
|
||||
collectionConfig,
|
||||
collections,
|
||||
columns: columnsFromArgs,
|
||||
columns,
|
||||
customCellProps,
|
||||
data,
|
||||
enableRowSelections,
|
||||
@@ -90,7 +88,7 @@ export const renderTable = ({
|
||||
clientConfig?: ClientConfig
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
collections?: string[]
|
||||
columns?: CollectionPreferences['columns']
|
||||
columns: ColumnPreference[]
|
||||
customCellProps?: Record<string, unknown>
|
||||
data?: PaginatedDocs | undefined
|
||||
drawerSlug?: string
|
||||
@@ -150,24 +148,6 @@ export const renderTable = ({
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnPreference[] = columnsFromArgs
|
||||
? columnsFromArgs?.filter((column) =>
|
||||
flattenTopLevelFields(clientFields, {
|
||||
i18n,
|
||||
keepPresentationalFields: true,
|
||||
moveSubFieldsToTop: true,
|
||||
})?.some((field) => {
|
||||
const accessor =
|
||||
'accessor' in field ? field.accessor : 'name' in field ? field.name : undefined
|
||||
return accessor === column.accessor
|
||||
}),
|
||||
)
|
||||
: getInitialColumns(
|
||||
isPolymorphic ? clientFields : filterFields(clientFields),
|
||||
useAsTitle,
|
||||
isPolymorphic ? [] : clientCollectionConfig?.admin?.defaultColumns,
|
||||
)
|
||||
|
||||
const sharedArgs: Pick<
|
||||
BuildColumnStateArgs,
|
||||
| 'clientFields'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
@@ -13,14 +15,11 @@ export const PostsCollection: CollectionConfig = {
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -126,12 +126,21 @@ export interface UserAuthOperations {
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
array?:
|
||||
| {
|
||||
title?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -270,12 +279,7 @@ export interface PayloadMigration {
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
id?: T;
|
||||
};
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useListQuery } from '@payloadcms/ui'
|
||||
|
||||
export const BeforeListTable = () => {
|
||||
const { data } = useListQuery()
|
||||
|
||||
return <p id="table-state">{JSON.stringify(data?.docs || [])}</p>
|
||||
}
|
||||
25
test/admin/collections/ListViewSelectAPI/index.ts
Normal file
25
test/admin/collections/ListViewSelectAPI/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const listViewSelectAPISlug = 'list-view-select-api'
|
||||
|
||||
export const ListViewSelectAPI: CollectionConfig = {
|
||||
slug: listViewSelectAPISlug,
|
||||
admin: {
|
||||
enableListViewSelectAPI: true,
|
||||
components: {
|
||||
beforeListTable: [
|
||||
'./collections/ListViewSelectAPI/BeforeListTable/index.tsx#BeforeListTable',
|
||||
],
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { CollectionGroup2A } from './collections/Group2A.js'
|
||||
import { CollectionGroup2B } from './collections/Group2B.js'
|
||||
import { CollectionHidden } from './collections/Hidden.js'
|
||||
import { ListDrawer } from './collections/ListDrawer.js'
|
||||
import { ListViewSelectAPI } from './collections/ListViewSelectAPI/index.js'
|
||||
import { CollectionNoApiView } from './collections/NoApiView.js'
|
||||
import { CollectionNotInView } from './collections/NotInView.js'
|
||||
import { Placeholder } from './collections/Placeholder.js'
|
||||
@@ -188,6 +189,7 @@ export default buildConfigWithDefaults({
|
||||
UseAsTitleGroupField,
|
||||
DisableBulkEdit,
|
||||
CustomListDrawer,
|
||||
ListViewSelectAPI,
|
||||
Virtuals,
|
||||
],
|
||||
globals: [
|
||||
|
||||
@@ -34,8 +34,10 @@ const description = 'Description'
|
||||
|
||||
let payload: PayloadTestSDK<Config>
|
||||
|
||||
import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js'
|
||||
import { devUser } from 'credentials.js'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js'
|
||||
import { goToNextPage, goToPreviousPage } from 'helpers/e2e/goToNextPage.js'
|
||||
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||
@@ -968,7 +970,7 @@ describe('List View', () => {
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should toggle columns and effect table', async () => {
|
||||
test('should toggle columns and affect table', async () => {
|
||||
const tableHeaders = 'table > thead > tr > th'
|
||||
|
||||
await openListColumns(page, {})
|
||||
@@ -992,6 +994,60 @@ describe('List View', () => {
|
||||
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
|
||||
})
|
||||
|
||||
test('should use select API in the list view when `enableListViewSelectAPI` is true', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: listViewSelectAPISlug,
|
||||
data: {
|
||||
title: 'This is a test title',
|
||||
description: 'This is a test description',
|
||||
},
|
||||
})
|
||||
|
||||
const selectAPIUrl = new AdminUrlUtil(serverURL, listViewSelectAPISlug)
|
||||
|
||||
await page.goto(selectAPIUrl.list)
|
||||
|
||||
const printedResults = page.locator('#table-state')
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const resultText = await printedResults.innerText()
|
||||
const parsedResult = JSON.parse(resultText)
|
||||
return Boolean(parsedResult[0].id && parsedResult[0].description)
|
||||
},
|
||||
{
|
||||
timeout: 3000,
|
||||
intervals: [100, 250, 500, 1000],
|
||||
},
|
||||
)
|
||||
.toBeTruthy()
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
|
||||
|
||||
await toggleColumn(page, {
|
||||
columnLabel: 'Description',
|
||||
columnName: 'description',
|
||||
targetState: 'off',
|
||||
})
|
||||
|
||||
// Poll until the "description" field is removed from the response BUT `id` is still present
|
||||
// The `id` field will remain selected despite it being inactive
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const resultText = await printedResults.innerText()
|
||||
const parsedResult = JSON.parse(resultText)
|
||||
return Boolean(parsedResult[0].description === undefined && parsedResult[0].id)
|
||||
},
|
||||
{
|
||||
timeout: 3000,
|
||||
intervals: [100, 250, 500, 1000],
|
||||
},
|
||||
)
|
||||
.toBeTruthy()
|
||||
})
|
||||
|
||||
test('should toggle columns and save to preferences', async () => {
|
||||
const tableHeaders = 'table > thead > tr > th'
|
||||
const numberOfColumns = await page.locator(tableHeaders).count()
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface Config {
|
||||
'use-as-title-group-field': UseAsTitleGroupField;
|
||||
'disable-bulk-edit': DisableBulkEdit;
|
||||
'custom-list-drawer': CustomListDrawer;
|
||||
'list-view-select-api': ListViewSelectApi;
|
||||
virtuals: Virtual;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
@@ -128,6 +129,7 @@ export interface Config {
|
||||
'use-as-title-group-field': UseAsTitleGroupFieldSelect<false> | UseAsTitleGroupFieldSelect<true>;
|
||||
'disable-bulk-edit': DisableBulkEditSelect<false> | DisableBulkEditSelect<true>;
|
||||
'custom-list-drawer': CustomListDrawerSelect<false> | CustomListDrawerSelect<true>;
|
||||
'list-view-select-api': ListViewSelectApiSelect<false> | ListViewSelectApiSelect<true>;
|
||||
virtuals: VirtualsSelect<false> | VirtualsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
@@ -584,6 +586,17 @@ export interface CustomListDrawer {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "list-view-select-api".
|
||||
*/
|
||||
export interface ListViewSelectApi {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtuals".
|
||||
@@ -711,6 +724,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'custom-list-drawer';
|
||||
value: string | CustomListDrawer;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'list-view-select-api';
|
||||
value: string | ListViewSelectApi;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'virtuals';
|
||||
value: string | Virtual;
|
||||
@@ -1129,6 +1146,16 @@ export interface CustomListDrawerSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "list-view-select-api_select".
|
||||
*/
|
||||
export interface ListViewSelectApiSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
description?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtuals_select".
|
||||
|
||||
@@ -22,7 +22,12 @@ export const assertRequestBody = async <T>(
|
||||
page: Page,
|
||||
options: {
|
||||
action: () => Promise<void> | void
|
||||
expect?: (requestBody: T) => boolean | Promise<boolean>
|
||||
expect?: (
|
||||
requestBody: T,
|
||||
requestHeaders: {
|
||||
[key: string]: string
|
||||
},
|
||||
) => boolean | Promise<boolean>
|
||||
requestMethod?: string
|
||||
url: string
|
||||
},
|
||||
@@ -43,7 +48,7 @@ export const assertRequestBody = async <T>(
|
||||
const parsedBody = JSON.parse(requestBody) as T
|
||||
|
||||
if (typeof options.expect === 'function') {
|
||||
expect(await options.expect(parsedBody)).toBeTruthy()
|
||||
expect(await options.expect(parsedBody, request.headers())).toBeTruthy()
|
||||
}
|
||||
|
||||
return parsedBody
|
||||
|
||||
Reference in New Issue
Block a user