feat: query presets (#11330)

Query Presets allow you to save and share filters, columns, and sort
orders for your collections. This is useful for reusing common or
complex filtering patterns and column configurations across your team.
Query Presets are defined on the fly by the users of your app, rather
than being hard coded into the Payload Config.

Here's a screen recording demonstrating the general workflow as it
relates to the list view. Query Presets are not exclusive to the admin
panel, however, as they could be useful in a number of other contexts
and environments.


https://github.com/user-attachments/assets/1fe1155e-ae78-4f59-9138-af352762a1d5

Each Query Preset is saved as a new record in the database under the
`payload-query-presets` collection. This will effectively make them
CRUDable and allows for an endless number of preset configurations. As
you make changes to filters, columns, limit, etc. you can choose to save
them as a new record and optionally share them with others.

Normal document-level access control will determine who can read,
update, and delete these records. Payload provides a set of sensible
defaults here, such as "only me", "everyone", and "specific users", but
you can also extend your own set of access rules on top of this, such as
"by role", etc. Access control is customizable at the operation-level,
for example you can set this to "everyone" can read, but "only me" can
update.

To enable the Query Presets within a particular collection, set
`enableQueryPresets` on that collection's config.

Here's an example:

```ts
{
  // ...
  enableQueryPresets: true
}
```

Once enabled, a new set of controls will appear within the list view of
the admin panel. This is where you can select and manage query presets.

General settings for Query Presets are configured under the root
`queryPresets` property. This is where you can customize the labels,
apply custom access control rules, etc.

Here's an example of how you might augment the access control properties
with your own custom rule to achieve RBAC:

```ts
{
  // ...
  queryPresets: {
    constraints: {
      read: [
        {
          label: 'Specific Roles',
          value: 'specificRoles',
          fields: [roles],
          access: ({ req: { user } }) => ({
            'access.update.roles': {
              in: [user?.roles],
            },
          }),
        },
      ],
    }
  }
}
```

Related: #4193 and #3092

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
Jacob Fletcher
2025-03-24 13:16:39 -04:00
committed by GitHub
parent bb14cc9b41
commit 998181b986
168 changed files with 6737 additions and 418 deletions

View File

@@ -309,6 +309,7 @@ jobs:
- fields__collections__Text - fields__collections__Text
- fields__collections__UI - fields__collections__UI
- fields__collections__Upload - fields__collections__Upload
- query-presets
- form-state - form-state
- live-preview - live-preview
- localization - localization

View File

@@ -474,7 +474,7 @@ Field: '/path/to/CustomArrayManagerField',
rows={[ rows={[
[ [
{ {
value: '**\\\`path\\\`**', value: '**\\`path\\`**',
}, },
{ {
value: 'The path to the array or block field', value: 'The path to the array or block field',
@@ -482,7 +482,7 @@ Field: '/path/to/CustomArrayManagerField',
], ],
[ [
{ {
value: '**\\\`rowIndex\\\`**', value: '**\\`rowIndex\\`**',
}, },
{ {
value: 'The index of the row to remove', value: 'The index of the row to remove',
@@ -561,7 +561,7 @@ Field: '/path/to/CustomArrayManagerField',
rows={[ rows={[
[ [
{ {
value: '**\\\`path\\\`**', value: '**\\`path\\`**',
}, },
{ {
value: 'The path to the array or block field', value: 'The path to the array or block field',
@@ -569,7 +569,7 @@ Field: '/path/to/CustomArrayManagerField',
], ],
[ [
{ {
value: '**\\\`rowIndex\\\`**', value: '**\\`rowIndex\\`**',
}, },
{ {
value: 'The index of the row to replace', value: 'The index of the row to replace',
@@ -577,7 +577,7 @@ Field: '/path/to/CustomArrayManagerField',
], ],
[ [
{ {
value: '**\\\`data\\\`**', value: '**\\`data\\`**',
}, },
{ {
value: 'The data to replace within the row', value: 'The data to replace within the row',
@@ -791,17 +791,18 @@ const MyComponent: React.FC = () => {
The `useListQuery` hook returns an object with the following properties: The `useListQuery` hook returns an object with the following properties:
| Property | Description | | Property | Description |
| ------------------------- | ------------------------------------------------------------------------ | | ------------------------- | -------------------------------------------------------------------------------------- |
| **`data`** | The data that is being displayed in the List View. | | **`data`** | The data that is being displayed in the List View. |
| **`defaultLimit`** | The default limit of items to display in the List View. | | **`defaultLimit`** | The default limit of items to display in the List View. |
| **`defaultSort`** | The default sort order of items in the List View. | | **`defaultSort`** | The default sort order of items in the List View. |
| **`handlePageChange`** | A method to handle page changes in the List View. | | **`handlePageChange`** | A method to handle page changes in the List View. |
| **`handlePerPageChange`** | A method to handle per page changes in the List View. | | **`handlePerPageChange`** | A method to handle per page changes in the List View. |
| **`handleSearchChange`** | A method to handle search changes in the List View. | | **`handleSearchChange`** | A method to handle search changes in the List View. |
| **`handleSortChange`** | A method to handle sort changes in the List View. | | **`handleSortChange`** | A method to handle sort changes in the List View. |
| **`handleWhereChange`** | A method to handle where changes in the List View. | | **`handleWhereChange`** | A method to handle where changes in the List View. |
| **`query`** | The current query that is being used to fetch the data in the List View. | | **`modified`** | Whether the query has been changed from its [Query Preset](../query-presets/overview). |
| **`query`** | The current query that is being used to fetch the data in the List View. |
## useSelection ## useSelection

View File

@@ -60,29 +60,30 @@ export const Posts: CollectionConfig = {
The following options are available: The following options are available:
| Option | Description | | Option | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). | | `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). | | `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). | | `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `custom` | Extension point for adding custom data (e.g. for plugins) | | `custom` | Extension point for adding custom data (e.g. for plugins) |
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. | | `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. | | `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. | | `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). | | `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). | | `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) | | `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). | | `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | | `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). | | `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. | | `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | | `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | | `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. | | `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#collection-config). | | `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). | | `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `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. | | `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `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 | | `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 |
_\* An asterisk denotes that a property is required._ _\* An asterisk denotes that a property is required._

View File

@@ -84,6 +84,7 @@ The following options are available:
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). | | **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). | | **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. | | **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). | | **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. | | **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). | | **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |

View File

@@ -0,0 +1,171 @@
---
title: Query Presets
label: Overview
order: 10
desc: Query Presets allow you to save and share filters, columns, and sort orders for your collections.
keywords:
---
Query Presets allow you to save and share filters, columns, and sort orders for your [Collections](../configuration/collections). This is useful for reusing common or complex filtering patterns and/or sharing them across your team.
Each Query Preset is saved as a new record in the database under the `payload-query-presets` collection. This allows for an endless number of preset configurations, where the users of your app define the presets that are most useful to them, rather than being hard coded into the Payload Config.
Within the [Admin Panel](../admin/overview), Query Presets are applied to the List View. When enabled, new controls are displayed for users to manage presets. Once saved, these presets can be loaded up at any time and optionally shared with others.
To enable Query Presets on a Collection, use the `enableQueryPresets` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
// highlight-start
enableQueryPresets: true,
// highlight-end
}
```
## Config Options
While not required, you may want to customize the behavior of Query Presets to suit your needs, such as add custom labels or access control rules.
Settings for Query Presets are managed on the `queryPresets` property at the root of your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
// highlight-start
queryPresets: {
// ...
},
// highlight-end
})
```
The following options are available for Query Presets:
| Option | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
| `labels` | Custom labels to use for the Query Presets collection. |
## Access Control
Query Presets are subject to the same [Access Control](../access-control/overview) as the rest of Payload. This means you can use the same patterns you are already familiar with to control who can read, update, and delete presets.
Access Control for Query Presets can be customized in two ways:
1. [Collection Access Control](#static-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
2. [Document Access Control](#dynamic-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
### Collection Access Control
Collection-level access control applies to _all_ presets within the Query Presets collection. Users cannot control these rules, they are written statically in your config.
To add Collection Access Control, use the `queryPresets.access` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
queryPresets: {
// ...
// highlight-start
access: {
read: ({ req: { user } }) =>
user ? user?.roles?.some((role) => role === 'admin') : false,
update: ({ req: { user } }) =>
user ? user?.roles?.some((role) => role === 'admin') : false,
},
// highlight-end
},
})
```
This example restricts all Query Presets to users with the role of `admin`.
<Banner type="warning">
**Note:** Custom access control will override the defaults on this collection,
including the requirement for a user to be authenticated. Be sure to include
any necessary checks in your custom rules unless you intend on making these
publicly accessible.
</Banner>
### Document Access Control
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each document.
When a user manages a preset, document-level access control options will be available to them in the Admin Panel for each operation.
By default, Payload provides a set of sensible defaults for all Query Presets, but you can customize these rules to suit your needs:
- **Only Me**: Only the user who created the preset can read, update, and delete it.
- **Everyone**: All users can read, update, and delete the preset.
- **Specific Users**: Only select users can read, update, and delete the preset.
#### Custom Access Control
You can augment the default access control rules with your own custom rules. This can be useful for creating more complex access control patterns that the defaults don't provide, such as for RBAC.
Adding custom access control rules requires:
1. A label to display in the dropdown
2. A set of fields to conditionally render when that option is selected
3. A function that returns the access control rules for that option
To do this, use the `queryPresets.constraints` property in your [Payload Config](../configuration/payload-config).
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
queryPresets: {
// ...
// highlight-start
constraints: {
read: {
label: 'Specific Roles',
value: 'specificRoles',
fields: [
{
name: 'roles',
type: 'select',
hasMany: true,
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
],
},
],
access: ({ req: { user } }) => ({
'access.read.roles': {
in: [user?.roles],
},
}),
},
// highlight-end
},
},
})
```
In this example, we've added a new option called `Specific Roles` that allows users to select from a list of roles. When this option is selected, the user will be prompted to select one or more roles from a list of options. The access control rule for this option is that the user operating on the preset must have one of the selected roles.
<Banner type="warning">
**Note:** Payload places your custom fields into the `access[operation]` field
group, so your rules will need to reflect this.
</Banner>
The following options are available for each constraint:
| Option | Description |
| -------- | ------------------------------------------------------------------------ |
| `label` | The label to display in the dropdown for this constraint. |
| `value` | The value to store in the database when this constraint is selected. |
| `fields` | An array of fields to render when this constraint is selected. |
| `access` | A function that determines the access control rules for this constraint. |

View File

@@ -28,6 +28,7 @@ export const renderDocumentHandler = async (args: {
initialState?: FormState initialState?: FormState
locale?: Locale locale?: Locale
overrideEntityVisibility?: boolean overrideEntityVisibility?: boolean
redirectAfterCreate?: boolean
redirectAfterDelete: boolean redirectAfterDelete: boolean
redirectAfterDuplicate: boolean redirectAfterDuplicate: boolean
req: PayloadRequest req: PayloadRequest
@@ -40,6 +41,7 @@ export const renderDocumentHandler = async (args: {
initialData, initialData,
locale, locale,
overrideEntityVisibility, overrideEntityVisibility,
redirectAfterCreate,
redirectAfterDelete, redirectAfterDelete,
redirectAfterDuplicate, redirectAfterDuplicate,
req, req,
@@ -165,6 +167,7 @@ export const renderDocumentHandler = async (args: {
segments: ['collections', collectionSlug, docID], segments: ['collections', collectionSlug, docID],
}, },
payload, payload,
redirectAfterCreate,
redirectAfterDelete, redirectAfterDelete,
redirectAfterDuplicate, redirectAfterDuplicate,
searchParams: {}, searchParams: {},

View File

@@ -44,6 +44,7 @@ export const renderDocument = async ({
initPageResult, initPageResult,
overrideEntityVisibility, overrideEntityVisibility,
params, params,
redirectAfterCreate,
redirectAfterDelete, redirectAfterDelete,
redirectAfterDuplicate, redirectAfterDuplicate,
searchParams, searchParams,
@@ -51,6 +52,9 @@ export const renderDocument = async ({
}: { }: {
drawerSlug?: string drawerSlug?: string
overrideEntityVisibility?: boolean overrideEntityVisibility?: boolean
readonly redirectAfterCreate?: boolean
readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean
} & AdminViewServerProps): Promise<{ } & AdminViewServerProps): Promise<{
data: Data data: Data
Document: React.ReactNode Document: React.ReactNode
@@ -308,7 +312,7 @@ export const renderDocument = async ({
id = doc.id id = doc.id
isEditing = getIsEditing({ id: doc.id, collectionSlug, globalSlug }) isEditing = getIsEditing({ id: doc.id, collectionSlug, globalSlug })
if (!drawerSlug) { if (!drawerSlug && redirectAfterCreate !== false) {
const redirectURL = formatAdminURL({ const redirectURL = formatAdminURL({
adminRoute, adminRoute,
path: `/collections/${collectionSlug}/${doc.id}`, path: `/collections/${collectionSlug}/${doc.id}`,
@@ -358,6 +362,7 @@ export const renderDocument = async ({
key={locale?.code} key={locale?.code}
lastUpdateTime={lastUpdateTime} lastUpdateTime={lastUpdateTime}
mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved} mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved}
redirectAfterCreate={redirectAfterCreate}
redirectAfterDelete={redirectAfterDelete} redirectAfterDelete={redirectAfterDelete}
redirectAfterDuplicate={redirectAfterDuplicate} redirectAfterDuplicate={redirectAfterDuplicate}
unpublishedVersionCount={unpublishedVersionCount} unpublishedVersionCount={unpublishedVersionCount}

View File

@@ -16,6 +16,7 @@ export const renderListHandler = async (args: {
disableActions?: boolean disableActions?: boolean
disableBulkDelete?: boolean disableBulkDelete?: boolean
disableBulkEdit?: boolean disableBulkEdit?: boolean
disableQueryPresets?: boolean
documentDrawerSlug: string documentDrawerSlug: string
drawerSlug?: string drawerSlug?: string
enableRowSelections: boolean enableRowSelections: boolean
@@ -30,6 +31,7 @@ export const renderListHandler = async (args: {
disableActions, disableActions,
disableBulkDelete, disableBulkDelete,
disableBulkEdit, disableBulkEdit,
disableQueryPresets,
drawerSlug, drawerSlug,
enableRowSelections, enableRowSelections,
overrideEntityVisibility, overrideEntityVisibility,
@@ -135,6 +137,7 @@ export const renderListHandler = async (args: {
disableActions, disableActions,
disableBulkDelete, disableBulkDelete,
disableBulkEdit, disableBulkEdit,
disableQueryPresets,
drawerSlug, drawerSlug,
enableRowSelections, enableRowSelections,
i18n, i18n,

View File

@@ -1,20 +1,29 @@
import type {
AdminViewServerProps,
ColumnPreference,
DefaultDocumentIDType,
ListPreferences,
ListQuery,
ListViewClientProps,
ListViewServerPropsOnly,
QueryPreset,
SanitizedCollectionPermission,
Where,
} from 'payload'
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui' import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc' import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
import { mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js' import { notFound } from 'next/navigation.js'
import { import {
type AdminViewServerProps, formatAdminURL,
type ColumnPreference, isNumber,
type ListPreferences, mergeListSearchAndWhere,
type ListQuery, transformColumnsToPreferences,
type ListViewClientProps, } from 'payload/shared'
type ListViewServerPropsOnly,
type Where,
} from 'payload'
import { formatAdminURL, isNumber, transformColumnsToPreferences } from 'payload/shared'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
import { renderListViewSlots } from './renderListViewSlots.js' import { renderListViewSlots } from './renderListViewSlots.js'
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js' import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
@@ -22,10 +31,13 @@ type RenderListViewArgs = {
customCellProps?: Record<string, any> customCellProps?: Record<string, any>
disableBulkDelete?: boolean disableBulkDelete?: boolean
disableBulkEdit?: boolean disableBulkEdit?: boolean
disableQueryPresets?: boolean
drawerSlug?: string drawerSlug?: string
enableRowSelections: boolean enableRowSelections: boolean
overrideEntityVisibility?: boolean overrideEntityVisibility?: boolean
query: ListQuery query: ListQuery
redirectAfterDelete?: boolean
redirectAfterDuplicate?: boolean
} & AdminViewServerProps } & AdminViewServerProps
export const renderListView = async ( export const renderListView = async (
@@ -38,6 +50,7 @@ export const renderListView = async (
customCellProps, customCellProps,
disableBulkDelete, disableBulkDelete,
disableBulkEdit, disableBulkEdit,
disableQueryPresets,
drawerSlug, drawerSlug,
enableRowSelections, enableRowSelections,
initPageResult, initPageResult,
@@ -85,6 +98,7 @@ export const renderListView = async (
value: { value: {
columns, columns,
limit: isNumber(query?.limit) ? Number(query.limit) : undefined, limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
preset: (query?.preset as DefaultDocumentIDType) || null,
sort: query?.sort as string, sort: query?.sort as string,
}, },
}) })
@@ -127,6 +141,32 @@ export const renderListView = async (
} }
} }
let queryPreset: QueryPreset | undefined
let queryPresetPermissions: SanitizedCollectionPermission | undefined
if (listPreferences?.preset) {
try {
queryPreset = (await payload.findByID({
id: listPreferences?.preset,
collection: 'payload-query-presets',
depth: 0,
overrideAccess: false,
user,
})) as QueryPreset
if (queryPreset) {
queryPresetPermissions = await getDocumentPermissions({
id: queryPreset.id,
collectionConfig: config.collections.find((c) => c.slug === 'payload-query-presets'),
data: queryPreset,
req,
})?.then(({ docPermissions }) => docPermissions)
}
} catch (err) {
req.payload.logger.error(`Error fetching query preset or preset permissions: ${err}`)
}
}
const data = await payload.find({ const data = await payload.find({
collection: collectionSlug, collection: collectionSlug,
depth: 0, depth: 0,
@@ -212,6 +252,7 @@ export const renderListView = async (
<Fragment> <Fragment>
<HydrateAuthProvider permissions={permissions} /> <HydrateAuthProvider permissions={permissions} />
<ListQueryProvider <ListQueryProvider
collectionSlug={collectionSlug}
columns={transformColumnsToPreferences(columnState)} columns={transformColumnsToPreferences(columnState)}
data={data} data={data}
defaultLimit={limit} defaultLimit={limit}
@@ -226,10 +267,13 @@ export const renderListView = async (
columnState, columnState,
disableBulkDelete, disableBulkDelete,
disableBulkEdit, disableBulkEdit,
disableQueryPresets,
enableRowSelections, enableRowSelections,
hasCreatePermission, hasCreatePermission,
listPreferences, listPreferences,
newDocumentURL, newDocumentURL,
queryPreset,
queryPresetPermissions,
renderedFilters, renderedFilters,
resolvedFilterOptions, resolvedFilterOptions,
Table, Table,

View File

@@ -45,6 +45,7 @@ export const renderListViewSlots = ({
} }
const listMenuItems = collectionConfig.admin.components?.listMenuItems const listMenuItems = collectionConfig.admin.components?.listMenuItems
if (Array.isArray(listMenuItems)) { if (Array.isArray(listMenuItems)) {
result.listMenuItems = [ result.listMenuItems = [
RenderServerComponent({ RenderServerComponent({

View File

@@ -3,7 +3,7 @@ import type { Field } from '../../fields/config/types.js'
import type { ClientFieldWithOptionalType, ServerComponentProps } from './Field.js' import type { ClientFieldWithOptionalType, ServerComponentProps } from './Field.js'
export type GenericLabelProps = { export type GenericLabelProps = {
readonly as?: 'label' | 'span' readonly as?: 'h3' | 'label' | 'span'
readonly hideLocale?: boolean readonly hideLocale?: boolean
readonly htmlFor?: string readonly htmlFor?: string
readonly label?: StaticLabel readonly label?: StaticLabel

View File

@@ -41,11 +41,12 @@ export type ServerFunctionHandler = (
export type ListQuery = { export type ListQuery = {
/* /*
* This is an of strings, i.e. `['title', '-slug']` * This is an of strings, i.e. `['title', '-slug']`
* Use `transformColumnsToPreferences` to convert it back and forth * Use `transformColumnsToPreferences` and `transformColumnsToSearchParams` to convert it back and forth
*/ */
columns?: ColumnsFromURL columns?: ColumnsFromURL
limit?: string limit?: string
page?: string page?: string
preset?: number | string
/* /*
When provided, is automatically injected into the `where` object When provided, is automatically injected into the `where` object
*/ */

View File

@@ -45,8 +45,6 @@ export type AdminViewServerPropsOnly = {
readonly importMap: ImportMap readonly importMap: ImportMap
readonly initialData?: Data readonly initialData?: Data
readonly initPageResult: InitPageResult readonly initPageResult: InitPageResult
readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean
} & ServerProps } & ServerProps
export type AdminViewServerProps = AdminViewClientProps & AdminViewServerPropsOnly export type AdminViewServerProps = AdminViewClientProps & AdminViewServerPropsOnly

View File

@@ -1,9 +1,11 @@
import type { SanitizedCollectionPermission } from '../../auth/types.js'
import type { import type {
CollectionAdminOptions, CollectionAdminOptions,
SanitizedCollectionConfig, SanitizedCollectionConfig,
} from '../../collections/config/types.js' } from '../../collections/config/types.js'
import type { ServerProps } from '../../config/types.js' import type { ServerProps } from '../../config/types.js'
import type { ListPreferences } from '../../preferences/types.js' import type { ListPreferences } from '../../preferences/types.js'
import type { QueryPreset } from '../../query-presets/types.js'
import type { ResolvedFilterOptions } from '../../types/index.js' import type { ResolvedFilterOptions } from '../../types/index.js'
import type { Column } from '../elements/Table.js' import type { Column } from '../elements/Table.js'
import type { Data } from '../types.js' import type { Data } from '../types.js'
@@ -40,6 +42,7 @@ export type ListViewClientProps = {
columnState: Column[] columnState: Column[]
disableBulkDelete?: boolean disableBulkDelete?: boolean
disableBulkEdit?: boolean disableBulkEdit?: boolean
disableQueryPresets?: boolean
enableRowSelections?: boolean enableRowSelections?: boolean
hasCreatePermission: boolean hasCreatePermission: boolean
/** /**
@@ -51,6 +54,8 @@ export type ListViewClientProps = {
* @deprecated * @deprecated
*/ */
preferenceKey?: string preferenceKey?: string
queryPreset?: QueryPreset
queryPresetPermissions?: SanitizedCollectionPermission
renderedFilters?: Map<string, React.ReactNode> renderedFilters?: Map<string, React.ReactNode>
resolvedFilterOptions?: Map<string, ResolvedFilterOptions> resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
} & ListViewSlots } & ListViewSlots

View File

@@ -1,5 +1,6 @@
import type { PayloadComponent } from '../../../config/types.js' import type { PayloadComponent } from '../../../config/types.js'
import type { ImportMap } from '../index.js' import type { ImportMap } from '../index.js'
import { parsePayloadComponent } from './parsePayloadComponent.js' import { parsePayloadComponent } from './parsePayloadComponent.js'
export const getFromImportMap = <TOutput>(args: { export const getFromImportMap = <TOutput>(args: {

View File

@@ -420,6 +420,11 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
* When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs * When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs
*/ */
disableDuplicate?: boolean disableDuplicate?: boolean
/**
* Opt-in to enable query presets for this collection.
* @see https://payloadcms.com/docs/query-presets/overview
*/
enableQueryPresets?: boolean
/** /**
* Custom rest api endpoints, set false to disable all rest endpoints for this collection. * Custom rest api endpoints, set false to disable all rest endpoints for this collection.
*/ */

View File

@@ -17,6 +17,7 @@ import {
} from '../collections/config/client.js' } from '../collections/config/client.js'
import { createClientBlocks } from '../fields/config/client.js' import { createClientBlocks } from '../fields/config/client.js'
import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/config/client.js' import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/config/client.js'
export type ServerOnlyRootProperties = keyof Pick< export type ServerOnlyRootProperties = keyof Pick<
SanitizedConfig, SanitizedConfig,
| 'bin' | 'bin'
@@ -34,6 +35,7 @@ export type ServerOnlyRootProperties = keyof Pick<
| 'logger' | 'logger'
| 'onInit' | 'onInit'
| 'plugins' | 'plugins'
| 'queryPresets'
| 'secret' | 'secret'
| 'sharp' | 'sharp'
| 'typescript' | 'typescript'
@@ -83,6 +85,7 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
'graphQL', 'graphQL',
'jobs', 'jobs',
'logger', 'logger',
'queryPresets',
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately // `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
] ]

View File

@@ -31,6 +31,7 @@ import {
lockedDocumentsCollectionSlug, lockedDocumentsCollectionSlug,
} from '../locked-documents/config.js' } from '../locked-documents/config.js'
import { getPreferencesCollection, preferencesCollectionSlug } from '../preferences/config.js' import { getPreferencesCollection, preferencesCollectionSlug } from '../preferences/config.js'
import { getQueryPresetsConfig, queryPresetsCollectionSlug } from '../query-presets/config.js'
import { getDefaultJobsCollection, jobsCollectionSlug } from '../queues/config/index.js' import { getDefaultJobsCollection, jobsCollectionSlug } from '../queues/config/index.js'
import { flattenBlock } from '../utilities/flattenAllFields.js' import { flattenBlock } from '../utilities/flattenAllFields.js'
import { getSchedulePublishTask } from '../versions/schedule/job.js' import { getSchedulePublishTask } from '../versions/schedule/job.js'
@@ -176,6 +177,9 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
const richTextSanitizationPromises: Array<(config: SanitizedConfig) => Promise<void>> = [] const richTextSanitizationPromises: Array<(config: SanitizedConfig) => Promise<void>> = []
const schedulePublishCollections: CollectionSlug[] = [] const schedulePublishCollections: CollectionSlug[] = []
const queryPresetsCollections: CollectionSlug[] = []
const schedulePublishGlobals: GlobalSlug[] = [] const schedulePublishGlobals: GlobalSlug[] = []
const collectionSlugs = new Set<CollectionSlug>() const collectionSlugs = new Set<CollectionSlug>()
@@ -192,6 +196,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
* to be populated with the sanitized blocks * to be populated with the sanitized blocks
*/ */
config.blocks = [] config.blocks = []
if (incomingConfig.blocks?.length) { if (incomingConfig.blocks?.length) {
for (const block of incomingConfig.blocks) { for (const block of incomingConfig.blocks) {
const sanitizedBlock = block const sanitizedBlock = block
@@ -206,6 +211,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
sanitizedBlock.labels = !sanitizedBlock.labels sanitizedBlock.labels = !sanitizedBlock.labels
? formatLabels(sanitizedBlock.slug) ? formatLabels(sanitizedBlock.slug)
: sanitizedBlock.labels : sanitizedBlock.labels
sanitizedBlock.fields = await sanitizeFields({ sanitizedBlock.fields = await sanitizeFields({
config: config as unknown as Config, config: config as unknown as Config,
existingFieldNames: new Set(), existingFieldNames: new Set(),
@@ -234,6 +240,14 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
schedulePublishCollections.push(config.collections[i].slug) schedulePublishCollections.push(config.collections[i].slug)
} }
if (config.collections[i].enableQueryPresets) {
queryPresetsCollections.push(config.collections[i].slug)
if (!validRelationships.includes(queryPresetsCollectionSlug)) {
validRelationships.push(queryPresetsCollectionSlug)
}
}
config.collections[i] = await sanitizeCollection( config.collections[i] = await sanitizeCollection(
config as unknown as Config, config as unknown as Config,
config.collections[i], config.collections[i],
@@ -312,35 +326,38 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
} }
} }
const lockedDocumentsCollection = getLockedDocumentsCollection(config as unknown as Config) configWithDefaults.collections.push(
if (lockedDocumentsCollection) { await sanitizeCollection(
configWithDefaults.collections.push( config as unknown as Config,
await sanitizeCollection( getLockedDocumentsCollection(config as unknown as Config),
config as unknown as Config, richTextSanitizationPromises,
getLockedDocumentsCollection(config as unknown as Config), validRelationships,
richTextSanitizationPromises, ),
validRelationships, )
),
)
}
const preferencesCollection = getPreferencesCollection(config as unknown as Config) configWithDefaults.collections.push(
if (preferencesCollection) { await sanitizeCollection(
configWithDefaults.collections.push( config as unknown as Config,
await sanitizeCollection( getPreferencesCollection(config as unknown as Config),
config as unknown as Config, richTextSanitizationPromises,
getPreferencesCollection(config as unknown as Config), validRelationships,
richTextSanitizationPromises, ),
validRelationships, )
),
)
}
if (migrationsCollection) { configWithDefaults.collections.push(
await sanitizeCollection(
config as unknown as Config,
migrationsCollection,
richTextSanitizationPromises,
validRelationships,
),
)
if (queryPresetsCollections.length > 0) {
configWithDefaults.collections.push( configWithDefaults.collections.push(
await sanitizeCollection( await sanitizeCollection(
config as unknown as Config, config as unknown as Config,
migrationsCollection, getQueryPresetsConfig(config as unknown as Config),
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,
), ),
@@ -380,9 +397,11 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
} }
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
for (const sanitizeFunction of richTextSanitizationPromises) { for (const sanitizeFunction of richTextSanitizationPromises) {
promises.push(sanitizeFunction(config as SanitizedConfig)) promises.push(sanitizeFunction(config as SanitizedConfig))
} }
await Promise.all(promises) await Promise.all(promises)
return config as SanitizedConfig return config as SanitizedConfig

View File

@@ -49,6 +49,7 @@ import type {
RequestContext, RequestContext,
TypedUser, TypedUser,
} from '../index.js' } from '../index.js'
import type { QueryPreset, QueryPresetConstraints } from '../query-presets/types.js'
import type { PayloadRequest, Where } from '../types/index.js' import type { PayloadRequest, Where } from '../types/index.js'
import type { PayloadLogger } from '../utilities/logger.js' import type { PayloadLogger } from '../utilities/logger.js'
@@ -944,12 +945,12 @@ export type Config = {
cookiePrefix?: string cookiePrefix?: string
/** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */ /** Either a whitelist array of URLS to allow CORS requests from, or a wildcard string ('*') to accept incoming requests from any domain. */
cors?: '*' | CORSConfig | string[] cors?: '*' | CORSConfig | string[]
/** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */ /** A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. */
csrf?: string[] csrf?: string[]
/** Extension point to add your custom data. Server only. */ /** Extension point to add your custom data. Server only. */
custom?: Record<string, any> custom?: Record<string, any>
/** Pass in a database adapter for use on this project. */ /** Pass in a database adapter for use on this project. */
db: DatabaseAdapterResult db: DatabaseAdapterResult
/** Enable to expose more detailed error information. */ /** Enable to expose more detailed error information. */
@@ -1039,7 +1040,6 @@ export type Config = {
* @default false // disable localization * @default false // disable localization
*/ */
localization?: false | LocalizationConfig localization?: false | LocalizationConfig
/** /**
* Logger options, logger options with a destination stream, or an instantiated logger instance. * Logger options, logger options with a destination stream, or an instantiated logger instance.
* *
@@ -1096,6 +1096,7 @@ export type Config = {
* @default 10 * @default 10
*/ */
maxDepth?: number maxDepth?: number
/** A function that is called immediately following startup that receives the Payload instance as its only argument. */ /** A function that is called immediately following startup that receives the Payload instance as its only argument. */
onInit?: (payload: Payload) => Promise<void> | void onInit?: (payload: Payload) => Promise<void> | void
/** /**
@@ -1104,6 +1105,25 @@ export type Config = {
* @see https://payloadcms.com/docs/plugins/overview * @see https://payloadcms.com/docs/plugins/overview
*/ */
plugins?: Plugin[] plugins?: Plugin[]
/**
* Allow you to save and share filters, columns, and sort orders for your collections.
* @see https://payloadcms.com/docs/query-presets/overview
*/
queryPresets?: {
access: {
create?: Access<QueryPreset>
delete?: Access<QueryPreset>
read?: Access<QueryPreset>
update?: Access<QueryPreset>
}
constraints: {
create?: QueryPresetConstraints
delete?: QueryPresetConstraints
read?: QueryPresetConstraints
update?: QueryPresetConstraints
}
labels?: CollectionConfig['labels']
}
/** Control the routing structure that Payload binds itself to. */ /** Control the routing structure that Payload binds itself to. */
routes?: { routes?: {
/** The route for the admin panel. /** The route for the admin panel.

View File

@@ -45,7 +45,6 @@ export { validOperators, validOperatorSet } from '../types/constants.js'
export { formatFilesize } from '../uploads/formatFilesize.js' export { formatFilesize } from '../uploads/formatFilesize.js'
export { isImage } from '../uploads/isImage.js' export { isImage } from '../uploads/isImage.js'
export { export {
deepCopyObject, deepCopyObject,
deepCopyObjectComplex, deepCopyObjectComplex,
@@ -61,13 +60,15 @@ export {
} from '../utilities/deepMerge.js' } from '../utilities/deepMerge.js'
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js' export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
export { flattenAllFields } from '../utilities/flattenAllFields.js' export { flattenAllFields } from '../utilities/flattenAllFields.js'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js' export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
export { formatAdminURL } from '../utilities/formatAdminURL.js' export { formatAdminURL } from '../utilities/formatAdminURL.js'
export { formatLabels, toWords } from '../utilities/formatLabels.js'
export { getDataByPath } from '../utilities/getDataByPath.js' export { getDataByPath } from '../utilities/getDataByPath.js'
export { getFieldPermissions } from '../utilities/getFieldPermissions.js' export { getFieldPermissions } from '../utilities/getFieldPermissions.js'
export { getSelectMode } from '../utilities/getSelectMode.js' export { getSelectMode } from '../utilities/getSelectMode.js'
export { getSiblingData } from '../utilities/getSiblingData.js' export { getSiblingData } from '../utilities/getSiblingData.js'
@@ -86,6 +87,11 @@ export {
isReactServerComponentOrFunction, isReactServerComponentOrFunction,
} from '../utilities/isReactComponent.js' } from '../utilities/isReactComponent.js'
export {
hoistQueryParamsToAnd,
mergeListSearchAndWhere,
} from '../utilities/mergeListSearchAndWhere.js'
export { reduceFieldsToValues } from '../utilities/reduceFieldsToValues.js' export { reduceFieldsToValues } from '../utilities/reduceFieldsToValues.js'
export { setsAreEqual } from '../utilities/setsAreEqual.js' export { setsAreEqual } from '../utilities/setsAreEqual.js'
@@ -97,8 +103,11 @@ export {
transformColumnsToSearchParams, transformColumnsToSearchParams,
} from '../utilities/transformColumnPreferences.js' } from '../utilities/transformColumnPreferences.js'
export { transformWhereQuery } from '../utilities/transformWhereQuery.js'
export { unflatten } from '../utilities/unflatten.js' export { unflatten } from '../utilities/unflatten.js'
export { validateMimeType } from '../utilities/validateMimeType.js' export { validateMimeType } from '../utilities/validateMimeType.js'
export { validateWhereQuery } from '../utilities/validateWhereQuery.js'
export { wait } from '../utilities/wait.js' export { wait } from '../utilities/wait.js'
export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex.js' export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex.js'
export { versionDefaults } from '../versions/defaults.js' export { versionDefaults } from '../versions/defaults.js'

View File

@@ -1391,6 +1391,7 @@ export type {
PreferenceUpdateRequest, PreferenceUpdateRequest,
TabsPreferences, TabsPreferences,
} from './preferences/types.js' } from './preferences/types.js'
export type { QueryPreset } from './query-presets/types.js'
export { jobAfterRead } from './queues/config/index.js' export { jobAfterRead } from './queues/config/index.js'
export type { JobsConfig, RunJobAccess, RunJobAccessArgs } from './queues/config/types/index.js' export type { JobsConfig, RunJobAccess, RunJobAccessArgs } from './queues/config/types/index.js'

View File

@@ -1,3 +1,4 @@
import type { DefaultDocumentIDType } from '../index.js'
import type { PayloadRequest } from '../types/index.js' import type { PayloadRequest } from '../types/index.js'
export type PreferenceRequest = { export type PreferenceRequest = {
@@ -36,5 +37,6 @@ export type ColumnPreference = {
export type ListPreferences = { export type ListPreferences = {
columns?: ColumnPreference[] columns?: ColumnPreference[]
limit?: number limit?: number
preset?: DefaultDocumentIDType
sort?: string sort?: string
} }

View File

@@ -0,0 +1,94 @@
import type { Access, Config } from '../config/types.js'
import type { Operation } from '../types/index.js'
import defaultAccess from '../auth/defaultAccess.js'
const operations: Operation[] = ['delete', 'read', 'update', 'create'] as const
const defaultCollectionAccess = {
create: defaultAccess,
delete: defaultAccess,
read: defaultAccess,
unlock: defaultAccess,
update: defaultAccess,
}
export const getAccess = (config: Config): Record<Operation, Access> =>
operations.reduce(
(acc, operation) => {
acc[operation] = async (args) => {
const { req } = args
const collectionAccess = config?.queryPresets?.access?.[operation]
? await config.queryPresets.access[operation](args)
: defaultCollectionAccess?.[operation]
? defaultCollectionAccess[operation](args)
: true
// If collection-level access control is `false`, no need to continue to document-level access
if (collectionAccess === false) {
return false
}
// The `create` operation does not affect the document-level access control
if (operation === 'create') {
return collectionAccess
}
return {
and: [
{
or: [
// Default access control ensures a user exists, but custom access control may not
...(req?.user
? [
{
and: [
{
[`access.${operation}.users`]: {
in: [req.user.id],
},
},
{
[`access.${operation}.constraint`]: {
in: ['onlyMe', 'specificUsers'],
},
},
],
},
]
: []),
{
[`access.${operation}.constraint`]: {
equals: 'everyone',
},
},
...(await Promise.all(
(config?.queryPresets?.constraints?.[operation] || []).map(async (constraint) => {
const constraintAccess = constraint.access
? await constraint.access(args)
: undefined
return {
and: [
...(typeof constraintAccess === 'object' ? [constraintAccess] : []),
{
[`access.${operation}.constraint`]: {
equals: constraint.value,
},
},
],
}
}),
)),
],
},
...(typeof collectionAccess === 'object' ? [collectionAccess] : []),
],
}
}
return acc
},
{} as Record<Operation, Access>,
)

View File

@@ -0,0 +1,164 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Config } from '../config/types.js'
import type { Option } from '../fields/config/types.js'
import { transformWhereQuery } from '../utilities/transformWhereQuery.js'
import { validateWhereQuery } from '../utilities/validateWhereQuery.js'
import { getAccess } from './access.js'
import { getConstraints } from './constraints.js'
import { operations, type QueryPreset } from './types.js'
export const queryPresetsCollectionSlug = 'payload-query-presets'
export const getQueryPresetsConfig = (config: Config): CollectionConfig => ({
slug: queryPresetsCollectionSlug,
access: getAccess(config),
admin: {
defaultColumns: ['title', 'isShared', 'access', 'where', 'columns'],
hidden: true,
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'isShared',
type: 'checkbox',
defaultValue: false,
validate: (isShared, { data }) => {
const typedData = data as QueryPreset
// ensure the `isShared` is only true if all constraints are 'onlyMe'
if (typedData?.access) {
const someOperationsAreShared = Object.values(typedData.access).some(
(operation) => operation.constraint !== 'onlyMe',
)
if (!isShared && someOperationsAreShared) {
return 'If any constraint is not "onlyMe", the preset must be shared'
}
}
return true
},
},
getConstraints(config),
{
name: 'where',
type: 'json',
admin: {
components: {
Cell: '@payloadcms/ui#QueryPresetsWhereCell',
Field: '@payloadcms/ui#QueryPresetsWhereField',
},
},
hooks: {
beforeValidate: [
({ data }) => {
// transform the "where" query here so that the client-side doesn't have to
if (data?.where) {
if (validateWhereQuery(data.where)) {
return data.where
} else {
return transformWhereQuery(data.where)
}
}
return data?.where
},
],
},
label: 'Filters',
},
{
name: 'columns',
type: 'json',
admin: {
components: {
Cell: '@payloadcms/ui#QueryPresetsColumnsCell',
Field: '@payloadcms/ui#QueryPresetsColumnField',
},
},
validate: (value) => {
if (value) {
try {
JSON.parse(JSON.stringify(value))
} catch {
return 'Invalid JSON'
}
}
return true
},
},
{
name: 'relatedCollection',
type: 'select',
admin: {
hidden: true,
},
options: config.collections
? config.collections.reduce((acc, collection) => {
if (collection.enableQueryPresets) {
acc.push({
label: collection.labels?.plural || collection.slug,
value: collection.slug,
})
}
return acc
}, [] as Option[])
: [],
required: true,
},
],
hooks: {
beforeValidate: [
({ data, operation, req }) => {
// TODO: type this
const typedData = data as any
if (operation === 'create' || operation === 'update') {
// Ensure all operations have a constraint
operations.forEach((operation) => {
if (!typedData.access) {
typedData.access = {}
}
if (!typedData.access?.[operation]) {
typedData[operation] = {}
}
// Ensure all operations have a constraint
if (!typedData.access[operation]?.constraint) {
typedData.access[operation] = {
...typedData.access[operation],
constraint: 'onlyMe',
}
}
})
// If at least one constraint is not `onlyMe` then `isShared` must be true
if (typedData?.access) {
const someOperationsAreShared = Object.values(typedData.access).some(
// TODO: remove the `any` here
(operation: any) => operation.constraint !== 'onlyMe',
)
typedData.isShared = someOperationsAreShared
}
}
return typedData
},
],
},
labels: {
plural: 'Presets',
singular: 'Preset',
...(config.queryPresets?.labels || {}),
},
lockDocuments: false,
})

View File

@@ -0,0 +1,104 @@
import { getTranslation } from '@payloadcms/translations'
import type { Config } from '../config/types.js'
import type { Field } from '../fields/config/types.js'
import { fieldAffectsData } from '../fields/config/types.js'
import { toWords } from '../utilities/formatLabels.js'
import { operations, type QueryPresetConstraint } from './types.js'
export const getConstraints = (config: Config): Field => ({
name: 'access',
type: 'group',
admin: {
components: {
Cell: '@payloadcms/ui#QueryPresetsAccessCell',
},
condition: (data) => Boolean(data?.isShared),
},
fields: operations.map((operation) => ({
type: 'collapsible',
fields: [
{
name: operation,
type: 'group',
admin: {
hideGutter: true,
},
fields: [
{
name: 'constraint',
type: 'select',
defaultValue: 'onlyMe',
label: ({ i18n }) =>
`Specify who can ${operation} this ${getTranslation(config.queryPresets?.labels?.singular || 'Preset', i18n)}`,
options: [
{
label: 'Everyone',
value: 'everyone',
},
{
label: 'Only Me',
value: 'onlyMe',
},
{
label: 'Specific Users',
value: 'specificUsers',
},
...(config?.queryPresets?.constraints?.[operation]?.map(
(option: QueryPresetConstraint) => ({
label: option.label,
value: option.value,
}),
) || []),
],
},
{
name: 'users',
type: 'relationship',
admin: {
condition: (data) =>
Boolean(data?.access?.[operation]?.constraint === 'specificUsers'),
},
hasMany: true,
hooks: {
beforeChange: [
({ data, req }) => {
if (data?.access?.[operation]?.constraint === 'onlyMe') {
if (req.user) {
return [req.user.id]
}
}
return data?.access?.[operation]?.users
},
],
},
relationTo: 'users',
},
...(config?.queryPresets?.constraints?.[operation]?.reduce(
(acc: Field[], option: QueryPresetConstraint) => {
option.fields.forEach((field, index) => {
acc.push({ ...field })
if (fieldAffectsData(field)) {
acc[index].admin = {
...(acc[index]?.admin || {}),
condition: (data) =>
Boolean(data?.access?.[operation]?.constraint === option.value),
}
}
})
return acc
},
[] as Field[],
) || []),
],
label: false,
},
],
label: () => toWords(operation),
})),
label: 'Sharing settings',
})

View File

@@ -0,0 +1,33 @@
import type { Field } from '../fields/config/types.js'
import type { Access, CollectionSlug } from '../index.js'
import type { ListPreferences } from '../preferences/types.js'
import type { Where } from '../types/index.js'
// Note: order matters here as it will change the rendered order in the UI
export const operations = ['read', 'update', 'delete'] as const
type Operation = (typeof operations)[number]
export type QueryPreset = {
access: {
[operation in Operation]: {
constraint: 'everyone' | 'onlyMe' | 'specificUsers'
users?: string[]
}
}
columns: ListPreferences['columns']
id: number | string
isShared: boolean
relatedCollection: CollectionSlug
title: string
where: Where
}
export type QueryPresetConstraint = {
access: Access<QueryPreset>
fields: Field[]
label: string
value: string
}
export type QueryPresetConstraints = QueryPresetConstraint[]

View File

@@ -1,4 +1,6 @@
import type { ClientCollectionConfig, SanitizedCollectionConfig, Where } from 'payload' import type { ClientCollectionConfig } from '../collections/config/client.js'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { Where } from '../types/index.js'
const isEmptyObject = (obj: object) => Object.keys(obj).length === 0 const isEmptyObject = (obj: object) => Object.keys(obj).length === 0
@@ -11,7 +13,7 @@ export const hoistQueryParamsToAnd = (currentWhere: Where, incomingWhere: Where)
return incomingWhere return incomingWhere
} }
if ('and' in currentWhere) { if ('and' in currentWhere && currentWhere.and) {
currentWhere.and.push(incomingWhere) currentWhere.and.push(incomingWhere)
} else if ('or' in currentWhere) { } else if ('or' in currentWhere) {
currentWhere = { currentWhere = {

View File

@@ -1,15 +1,18 @@
'use client' import type { Where } from '../types/index.js'
import type { Where } from 'payload'
/** /**
* Something like [or][0][and][0][text][equals]=example%20post will work and pass through the validateWhereQuery check. * Transforms a basic "where" query into a format in which the "where builder" can understand.
* However, something like [text][equals]=example%20post will not work and will fail the validateWhereQuery check, * Even though basic queries are valid, we need to hoist them into the "and" / "or" format.
* even though it is a valid Where query. This needs to be transformed here. * Use this function alongside `validateWhereQuery` to check that for valid queries before transforming.
* @example
* Inaccurate: [text][equals]=example%20post
* Accurate: [or][0][and][0][text][equals]=example%20post
*/ */
export const transformWhereQuery = (whereQuery): Where => { export const transformWhereQuery = (whereQuery: Where): Where => {
if (!whereQuery) { if (!whereQuery) {
return {} return {}
} }
// Check if 'whereQuery' has 'or' field but no 'and'. This is the case for "correct" queries // Check if 'whereQuery' has 'or' field but no 'and'. This is the case for "correct" queries
if (whereQuery.or && !whereQuery.and) { if (whereQuery.or && !whereQuery.and) {
return { return {

View File

@@ -1,10 +1,18 @@
'use client' import type { Operator, Where } from '../types/index.js'
import type { Operator, Where } from 'payload'
import { validOperatorSet } from 'payload/shared' import { validOperatorSet } from '../types/constants.js'
const validateWhereQuery = (whereQuery): whereQuery is Where => { /**
* Validates that a "where" query is in a format in which the "where builder" can understand.
* Even though basic queries are valid, we need to hoist them into the "and" / "or" format.
* Use this function alongside `transformWhereQuery` to perform a transformation if the query is not valid.
* @example
* Inaccurate: [text][equals]=example%20post
* Accurate: [or][0][and][0][text][equals]=example%20post
*/
export const validateWhereQuery = (whereQuery: Where): whereQuery is Where => {
if ( if (
whereQuery?.or &&
whereQuery?.or?.length > 0 && whereQuery?.or?.length > 0 &&
whereQuery?.or?.[0]?.and && whereQuery?.or?.[0]?.and &&
whereQuery?.or?.[0]?.and?.length > 0 whereQuery?.or?.[0]?.and?.length > 0
@@ -44,5 +52,3 @@ const validateWhereQuery = (whereQuery): whereQuery is Where => {
return false return false
} }
export default validateWhereQuery

View File

@@ -252,6 +252,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:selectAll', 'general:selectAll',
'general:selectAllRows', 'general:selectAllRows',
'general:selectedCount', 'general:selectedCount',
'general:selectLabel',
'general:selectValue', 'general:selectValue',
'general:showAllLabel', 'general:showAllLabel',
'general:sorryNotFound', 'general:sorryNotFound',
@@ -280,7 +281,9 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:unsavedChangesDuplicate', 'general:unsavedChangesDuplicate',
'general:untitled', 'general:untitled',
'general:updatedAt', 'general:updatedAt',
'general:updatedLabelSuccessfully',
'general:updatedCountSuccessfully', 'general:updatedCountSuccessfully',
'general:updateForEveryone',
'general:updatedSuccessfully', 'general:updatedSuccessfully',
'general:updating', 'general:updating',
'general:value', 'general:value',

View File

@@ -311,6 +311,7 @@ export const arTranslations: DefaultTranslationsObject = {
selectAll: 'تحديد كل {{count}} {{label}}', selectAll: 'تحديد كل {{count}} {{label}}',
selectAllRows: 'حدد جميع الصفوف', selectAllRows: 'حدد جميع الصفوف',
selectedCount: 'تم تحديد {{count}} {{label}}', selectedCount: 'تم تحديد {{count}} {{label}}',
selectLabel: 'حدد {{label}}',
selectValue: 'اختيار قيمة', selectValue: 'اختيار قيمة',
showAllLabel: 'عرض كل {{label}}', showAllLabel: 'عرض كل {{label}}',
sorryNotFound: 'عذرًا - لا يوجد شيء يتوافق مع طلبك.', sorryNotFound: 'عذرًا - لا يوجد شيء يتوافق مع طلبك.',
@@ -338,7 +339,9 @@ export const arTranslations: DefaultTranslationsObject = {
upcomingEvents: 'الأحداث القادمة', upcomingEvents: 'الأحداث القادمة',
updatedAt: 'تم التحديث في', updatedAt: 'تم التحديث في',
updatedCountSuccessfully: 'تم تحديث {{count}} {{label}} بنجاح.', updatedCountSuccessfully: 'تم تحديث {{count}} {{label}} بنجاح.',
updatedLabelSuccessfully: 'تم تحديث {{label}} بنجاح.',
updatedSuccessfully: 'تم التحديث بنجاح.', updatedSuccessfully: 'تم التحديث بنجاح.',
updateForEveryone: 'تحديث للجميع',
updating: 'جار التحديث', updating: 'جار التحديث',
uploading: 'جار الرفع', uploading: 'جار الرفع',
uploadingBulk: 'جاري التحميل {{current}} من {{total}}', uploadingBulk: 'جاري التحميل {{current}} من {{total}}',

View File

@@ -314,6 +314,7 @@ export const azTranslations: DefaultTranslationsObject = {
selectAll: 'Bütün {{count}} {{label}} seç', selectAll: 'Bütün {{count}} {{label}} seç',
selectAllRows: 'Bütün sıraları seçin', selectAllRows: 'Bütün sıraları seçin',
selectedCount: '{{count}} {{label}} seçildi', selectedCount: '{{count}} {{label}} seçildi',
selectLabel: '{{label}} seçin',
selectValue: 'Dəyər seçin', selectValue: 'Dəyər seçin',
showAllLabel: 'Bütün {{label}}-ı göstər', showAllLabel: 'Bütün {{label}}-ı göstər',
sorryNotFound: 'Üzr istəyirik - sizin tələbinizə uyğun heç nə yoxdur.', sorryNotFound: 'Üzr istəyirik - sizin tələbinizə uyğun heç nə yoxdur.',
@@ -343,7 +344,9 @@ export const azTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Gələcək Tədbirlər', upcomingEvents: 'Gələcək Tədbirlər',
updatedAt: 'Yeniləndiyi tarix', updatedAt: 'Yeniləndiyi tarix',
updatedCountSuccessfully: '{{count}} {{label}} uğurla yeniləndi.', updatedCountSuccessfully: '{{count}} {{label}} uğurla yeniləndi.',
updatedLabelSuccessfully: '{{label}} uğurla yeniləndi.',
updatedSuccessfully: 'Uğurla yeniləndi.', updatedSuccessfully: 'Uğurla yeniləndi.',
updateForEveryone: 'Hər kəs üçün yeniləmə',
updating: 'Yenilənir', updating: 'Yenilənir',
uploading: 'Yüklənir', uploading: 'Yüklənir',
uploadingBulk: '{{total}}-dan {{current}}-un yüklənməsi', uploadingBulk: '{{total}}-dan {{current}}-un yüklənməsi',

View File

@@ -314,6 +314,7 @@ export const bgTranslations: DefaultTranslationsObject = {
selectAll: 'Избери всички {{count}} {{label}}', selectAll: 'Избери всички {{count}} {{label}}',
selectAllRows: 'Избери всички редове', selectAllRows: 'Избери всички редове',
selectedCount: '{{count}} {{label}} избрани', selectedCount: '{{count}} {{label}} избрани',
selectLabel: 'Изберете {{label}}',
selectValue: 'Избери стойност', selectValue: 'Избери стойност',
showAllLabel: 'Покажи всички {{label}}', showAllLabel: 'Покажи всички {{label}}',
sorryNotFound: 'Съжаляваме-няма нищо, което да отговаря на търсенето ти.', sorryNotFound: 'Съжаляваме-няма нищо, което да отговаря на търсенето ти.',
@@ -341,7 +342,9 @@ export const bgTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Предстоящи събития', upcomingEvents: 'Предстоящи събития',
updatedAt: 'Обновен на', updatedAt: 'Обновен на',
updatedCountSuccessfully: 'Обновени {{count}} {{label}} успешно.', updatedCountSuccessfully: 'Обновени {{count}} {{label}} успешно.',
updatedLabelSuccessfully: 'Успешно обновихме {{label}}.',
updatedSuccessfully: 'Обновен успешно.', updatedSuccessfully: 'Обновен успешно.',
updateForEveryone: 'Актуализация за всички',
updating: 'Обновява се', updating: 'Обновява се',
uploading: 'Качва се', uploading: 'Качва се',
uploadingBulk: 'Качване на {{current}} от {{total}}', uploadingBulk: 'Качване на {{current}} от {{total}}',

View File

@@ -315,6 +315,7 @@ export const caTranslations: DefaultTranslationsObject = {
selectAll: 'Selecciona totes les {{count}} {{label}}', selectAll: 'Selecciona totes les {{count}} {{label}}',
selectAllRows: 'Selecciona totes les files', selectAllRows: 'Selecciona totes les files',
selectedCount: '{{count}} {{label}} seleccionats', selectedCount: '{{count}} {{label}} seleccionats',
selectLabel: 'Selecciona {{label}}',
selectValue: 'Selecciona un valor', selectValue: 'Selecciona un valor',
showAllLabel: 'Mostra totes {{label}}', showAllLabel: 'Mostra totes {{label}}',
sorryNotFound: "Ho sento, no s'ha trobat la pàgina que busques.", sorryNotFound: "Ho sento, no s'ha trobat la pàgina que busques.",
@@ -342,7 +343,9 @@ export const caTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Esdeveniments programats', upcomingEvents: 'Esdeveniments programats',
updatedAt: 'Actualitzat el', updatedAt: 'Actualitzat el',
updatedCountSuccessfully: 'Actualitzat {{count}} {{label}} correctament.', updatedCountSuccessfully: 'Actualitzat {{count}} {{label}} correctament.',
updatedLabelSuccessfully: 'Actualitzat {{label}} amb èxit.',
updatedSuccessfully: 'Actualitzat amb exit.', updatedSuccessfully: 'Actualitzat amb exit.',
updateForEveryone: 'Actualització per a tothom',
updating: 'Actualitzant', updating: 'Actualitzant',
uploading: 'Pujant', uploading: 'Pujant',
uploadingBulk: 'Pujant {{current}} de {{total}}', uploadingBulk: 'Pujant {{current}} de {{total}}',

View File

@@ -312,6 +312,7 @@ export const csTranslations: DefaultTranslationsObject = {
selectAll: 'Vybrat vše {{count}} {{label}}', selectAll: 'Vybrat vše {{count}} {{label}}',
selectAllRows: 'Vyberte všechny řádky', selectAllRows: 'Vyberte všechny řádky',
selectedCount: 'Vybráno {{count}} {{label}}', selectedCount: 'Vybráno {{count}} {{label}}',
selectLabel: 'Vyberte {{label}}',
selectValue: 'Vyberte hodnotu', selectValue: 'Vyberte hodnotu',
showAllLabel: 'Zobrazit všechny {{label}}', showAllLabel: 'Zobrazit všechny {{label}}',
sorryNotFound: 'Je nám líto, ale neexistuje nic, co by odpovídalo vašemu požadavku.', sorryNotFound: 'Je nám líto, ale neexistuje nic, co by odpovídalo vašemu požadavku.',
@@ -339,7 +340,9 @@ export const csTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Nadcházející události', upcomingEvents: 'Nadcházející události',
updatedAt: 'Aktualizováno v', updatedAt: 'Aktualizováno v',
updatedCountSuccessfully: 'Úspěšně aktualizováno {{count}} {{label}}.', updatedCountSuccessfully: 'Úspěšně aktualizováno {{count}} {{label}}.',
updatedLabelSuccessfully: 'Úspěšně aktualizovaný {{label}}.',
updatedSuccessfully: 'Úspěšně aktualizováno.', updatedSuccessfully: 'Úspěšně aktualizováno.',
updateForEveryone: 'Aktualizace pro všechny',
updating: 'Aktualizace', updating: 'Aktualizace',
uploading: 'Nahrávání', uploading: 'Nahrávání',
uploadingBulk: 'Nahrávání {{current}} z {{total}}', uploadingBulk: 'Nahrávání {{current}} z {{total}}',

View File

@@ -313,6 +313,7 @@ export const daTranslations: DefaultTranslationsObject = {
selectAll: 'Vælg alle {{count}} {{label}}', selectAll: 'Vælg alle {{count}} {{label}}',
selectAllRows: 'Vælg alle rækker', selectAllRows: 'Vælg alle rækker',
selectedCount: '{{count}} {{label}} valgt', selectedCount: '{{count}} {{label}} valgt',
selectLabel: 'Vælg {{label}}',
selectValue: 'Vælg en værdi', selectValue: 'Vælg en værdi',
showAllLabel: 'Vis alle {{label}}', showAllLabel: 'Vis alle {{label}}',
sorryNotFound: 'Beklager—der er intet, der svarer til din handling.', sorryNotFound: 'Beklager—der er intet, der svarer til din handling.',
@@ -340,7 +341,9 @@ export const daTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Kommende Begivenheder', upcomingEvents: 'Kommende Begivenheder',
updatedAt: 'Opdateret ved', updatedAt: 'Opdateret ved',
updatedCountSuccessfully: 'Opdateret {{count}} {{label}} successfully.', updatedCountSuccessfully: 'Opdateret {{count}} {{label}} successfully.',
updatedLabelSuccessfully: 'Opdaterede {{label}} med succes.',
updatedSuccessfully: 'Opdateret.', updatedSuccessfully: 'Opdateret.',
updateForEveryone: 'Opdatering for alle',
updating: 'Opdaterer', updating: 'Opdaterer',
uploading: 'Uploader', uploading: 'Uploader',
uploadingBulk: 'Uploader {{current}} af {{total}}', uploadingBulk: 'Uploader {{current}} af {{total}}',

View File

@@ -318,6 +318,7 @@ export const deTranslations: DefaultTranslationsObject = {
selectAll: 'Alle auswählen {{count}} {{label}}', selectAll: 'Alle auswählen {{count}} {{label}}',
selectAllRows: 'Wählen Sie alle Zeilen aus', selectAllRows: 'Wählen Sie alle Zeilen aus',
selectedCount: '{{count}} {{label}} ausgewählt', selectedCount: '{{count}} {{label}} ausgewählt',
selectLabel: 'Wählen Sie {{label}}',
selectValue: 'Wert auswählen', selectValue: 'Wert auswählen',
showAllLabel: 'Zeige alle {{label}}', showAllLabel: 'Zeige alle {{label}}',
sorryNotFound: 'Entschuldige, es entspricht nichts deiner Anfrage', sorryNotFound: 'Entschuldige, es entspricht nichts deiner Anfrage',
@@ -347,7 +348,9 @@ export const deTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Bevorstehende Veranstaltungen', upcomingEvents: 'Bevorstehende Veranstaltungen',
updatedAt: 'Aktualisiert am', updatedAt: 'Aktualisiert am',
updatedCountSuccessfully: '{{count}} {{label}} erfolgreich aktualisiert.', updatedCountSuccessfully: '{{count}} {{label}} erfolgreich aktualisiert.',
updatedLabelSuccessfully: '{{label}} erfolgreich aktualisiert.',
updatedSuccessfully: 'Erfolgreich aktualisiert.', updatedSuccessfully: 'Erfolgreich aktualisiert.',
updateForEveryone: 'Aktualisierung für alle',
updating: 'Aktualisierung', updating: 'Aktualisierung',
uploading: 'Hochladen', uploading: 'Hochladen',
uploadingBulk: 'Hochladen von {{current}} von {{total}}', uploadingBulk: 'Hochladen von {{current}} von {{total}}',

View File

@@ -315,6 +315,7 @@ export const enTranslations = {
selectAll: 'Select all {{count}} {{label}}', selectAll: 'Select all {{count}} {{label}}',
selectAllRows: 'Select all rows', selectAllRows: 'Select all rows',
selectedCount: '{{count}} {{label}} selected', selectedCount: '{{count}} {{label}} selected',
selectLabel: 'Select {{label}}',
selectValue: 'Select a value', selectValue: 'Select a value',
showAllLabel: 'Show all {{label}}', showAllLabel: 'Show all {{label}}',
sorryNotFound: 'Sorry—there is nothing to correspond with your request.', sorryNotFound: 'Sorry—there is nothing to correspond with your request.',
@@ -342,7 +343,9 @@ export const enTranslations = {
upcomingEvents: 'Upcoming Events', upcomingEvents: 'Upcoming Events',
updatedAt: 'Updated At', updatedAt: 'Updated At',
updatedCountSuccessfully: 'Updated {{count}} {{label}} successfully.', updatedCountSuccessfully: 'Updated {{count}} {{label}} successfully.',
updatedLabelSuccessfully: 'Updated {{label}} successfully.',
updatedSuccessfully: 'Updated successfully.', updatedSuccessfully: 'Updated successfully.',
updateForEveryone: 'Update for everyone',
updating: 'Updating', updating: 'Updating',
uploading: 'Uploading', uploading: 'Uploading',
uploadingBulk: 'Uploading {{current}} of {{total}}', uploadingBulk: 'Uploading {{current}} of {{total}}',

View File

@@ -319,6 +319,7 @@ export const esTranslations: DefaultTranslationsObject = {
selectAll: 'Seleccionar todo {{count}} {{label}}', selectAll: 'Seleccionar todo {{count}} {{label}}',
selectAllRows: 'Selecciona todas las filas', selectAllRows: 'Selecciona todas las filas',
selectedCount: '{{count}} {{label}} seleccionado', selectedCount: '{{count}} {{label}} seleccionado',
selectLabel: 'Seleccione {{label}}',
selectValue: 'Selecciona un valor', selectValue: 'Selecciona un valor',
showAllLabel: 'Muestra todas {{label}}', showAllLabel: 'Muestra todas {{label}}',
sorryNotFound: 'Lo sentimos. No hay nada que corresponda con tu solicitud.', sorryNotFound: 'Lo sentimos. No hay nada que corresponda con tu solicitud.',
@@ -346,7 +347,9 @@ export const esTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Próximos Eventos', upcomingEvents: 'Próximos Eventos',
updatedAt: 'Fecha de modificado', updatedAt: 'Fecha de modificado',
updatedCountSuccessfully: '{{count}} {{label}} actualizado con éxito.', updatedCountSuccessfully: '{{count}} {{label}} actualizado con éxito.',
updatedLabelSuccessfully: 'Actualizado {{label}} con éxito.',
updatedSuccessfully: 'Actualizado con éxito.', updatedSuccessfully: 'Actualizado con éxito.',
updateForEveryone: 'Actualización para todos',
updating: 'Actualizando', updating: 'Actualizando',
uploading: 'Subiendo', uploading: 'Subiendo',
uploadingBulk: 'Subiendo {{current}} de {{total}}', uploadingBulk: 'Subiendo {{current}} de {{total}}',

View File

@@ -311,6 +311,7 @@ export const etTranslations: DefaultTranslationsObject = {
selectAll: 'Vali kõik {{count}} {{label}}', selectAll: 'Vali kõik {{count}} {{label}}',
selectAllRows: 'Vali kõik read', selectAllRows: 'Vali kõik read',
selectedCount: '{{count}} {{label}} valitud', selectedCount: '{{count}} {{label}} valitud',
selectLabel: 'Valige {{label}}',
selectValue: 'Vali väärtus', selectValue: 'Vali väärtus',
showAllLabel: 'Näita kõiki {{label}}', showAllLabel: 'Näita kõiki {{label}}',
sorryNotFound: 'Vabandust - teie päringule vastavat sisu ei leitud.', sorryNotFound: 'Vabandust - teie päringule vastavat sisu ei leitud.',
@@ -338,7 +339,9 @@ export const etTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Eelseisvad sündmused', upcomingEvents: 'Eelseisvad sündmused',
updatedAt: 'Uuendatud', updatedAt: 'Uuendatud',
updatedCountSuccessfully: 'Uuendatud {{count}} {{label}} edukalt.', updatedCountSuccessfully: 'Uuendatud {{count}} {{label}} edukalt.',
updatedLabelSuccessfully: 'Uuendas {{label}} edukalt.',
updatedSuccessfully: 'Edukalt uuendatud.', updatedSuccessfully: 'Edukalt uuendatud.',
updateForEveryone: 'Uuendus kõigile',
updating: 'Uuendamine', updating: 'Uuendamine',
uploading: 'Üleslaadimine', uploading: 'Üleslaadimine',
uploadingBulk: 'Üleslaadimine {{current}} / {{total}}', uploadingBulk: 'Üleslaadimine {{current}} / {{total}}',

View File

@@ -312,6 +312,7 @@ export const faTranslations: DefaultTranslationsObject = {
selectAll: 'انتخاب همه {{count}} {{label}}', selectAll: 'انتخاب همه {{count}} {{label}}',
selectAllRows: 'انتخاب تمام سطرها', selectAllRows: 'انتخاب تمام سطرها',
selectedCount: '{{count}} {{label}} انتخاب شد', selectedCount: '{{count}} {{label}} انتخاب شد',
selectLabel: '{{label}} را انتخاب کنید',
selectValue: 'یک مقدار را انتخاب کنید', selectValue: 'یک مقدار را انتخاب کنید',
showAllLabel: 'نمایش همه {{label}}', showAllLabel: 'نمایش همه {{label}}',
sorryNotFound: 'متأسفانه چیزی برای مطابقت با درخواست شما وجود ندارد.', sorryNotFound: 'متأسفانه چیزی برای مطابقت با درخواست شما وجود ندارد.',
@@ -339,7 +340,9 @@ export const faTranslations: DefaultTranslationsObject = {
upcomingEvents: 'رویدادهای آینده', upcomingEvents: 'رویدادهای آینده',
updatedAt: 'بروز شده در', updatedAt: 'بروز شده در',
updatedCountSuccessfully: 'تعداد {{count}} با عنوان {{label}} با موفقیت بروزرسانی شدند.', updatedCountSuccessfully: 'تعداد {{count}} با عنوان {{label}} با موفقیت بروزرسانی شدند.',
updatedLabelSuccessfully: 'به روزرسانی {{label}} با موفقیت انجام شد.',
updatedSuccessfully: 'با موفقیت به‌روز شد.', updatedSuccessfully: 'با موفقیت به‌روز شد.',
updateForEveryone: 'بروزرسانی برای همه',
updating: 'در حال به‌روزرسانی', updating: 'در حال به‌روزرسانی',
uploading: 'در حال بارگذاری', uploading: 'در حال بارگذاری',
uploadingBulk: 'بارگذاری {{current}} از {{total}}', uploadingBulk: 'بارگذاری {{current}} از {{total}}',

View File

@@ -322,6 +322,7 @@ export const frTranslations: DefaultTranslationsObject = {
selectAll: 'Tout sélectionner {{count}} {{label}}', selectAll: 'Tout sélectionner {{count}} {{label}}',
selectAllRows: 'Sélectionnez toutes les lignes', selectAllRows: 'Sélectionnez toutes les lignes',
selectedCount: '{{count}} {{label}} sélectionné', selectedCount: '{{count}} {{label}} sélectionné',
selectLabel: 'Sélectionnez {{label}}',
selectValue: 'Sélectionnez une valeur', selectValue: 'Sélectionnez une valeur',
showAllLabel: 'Afficher tous les {{label}}', showAllLabel: 'Afficher tous les {{label}}',
sorryNotFound: 'Désolé, rien ne correspond à votre demande.', sorryNotFound: 'Désolé, rien ne correspond à votre demande.',
@@ -351,7 +352,9 @@ export const frTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Événements à venir', upcomingEvents: 'Événements à venir',
updatedAt: 'Modifié le', updatedAt: 'Modifié le',
updatedCountSuccessfully: '{{count}} {{label}} mis à jour avec succès.', updatedCountSuccessfully: '{{count}} {{label}} mis à jour avec succès.',
updatedLabelSuccessfully: '{{label}} mis à jour avec succès.',
updatedSuccessfully: 'Mis à jour avec succès.', updatedSuccessfully: 'Mis à jour avec succès.',
updateForEveryone: 'Mise à jour pour tout le monde',
updating: 'Mise à jour', updating: 'Mise à jour',
uploading: 'Téléchargement', uploading: 'Téléchargement',
uploadingBulk: 'Téléchargement de {{current}} sur {{total}}', uploadingBulk: 'Téléchargement de {{current}} sur {{total}}',

View File

@@ -307,6 +307,7 @@ export const heTranslations: DefaultTranslationsObject = {
selectAll: 'בחר את כל {{count}} ה{{label}}', selectAll: 'בחר את כל {{count}} ה{{label}}',
selectAllRows: 'בחר את כל השורות', selectAllRows: 'בחר את כל השורות',
selectedCount: '{{count}} {{label}} נבחרו', selectedCount: '{{count}} {{label}} נבחרו',
selectLabel: '{{label}} בחר',
selectValue: 'בחר ערך', selectValue: 'בחר ערך',
showAllLabel: 'הצג את כל ה{{label}}', showAllLabel: 'הצג את כל ה{{label}}',
sorryNotFound: 'מצטערים - אין תוצאות התואמות את הבקשה.', sorryNotFound: 'מצטערים - אין תוצאות התואמות את הבקשה.',
@@ -334,7 +335,9 @@ export const heTranslations: DefaultTranslationsObject = {
upcomingEvents: 'אירועים קרובים', upcomingEvents: 'אירועים קרובים',
updatedAt: 'עודכן בתאריך', updatedAt: 'עודכן בתאריך',
updatedCountSuccessfully: 'עודכן {{count}} {{label}} בהצלחה.', updatedCountSuccessfully: 'עודכן {{count}} {{label}} בהצלחה.',
updatedLabelSuccessfully: 'עודכן {{label}} בהצלחה.',
updatedSuccessfully: 'עודכן בהצלחה.', updatedSuccessfully: 'עודכן בהצלחה.',
updateForEveryone: 'עדכון לכולם',
updating: 'מעדכן', updating: 'מעדכן',
uploading: 'מעלה', uploading: 'מעלה',
uploadingBulk: 'מעלה {{current}} מתוך {{total}}', uploadingBulk: 'מעלה {{current}} מתוך {{total}}',

View File

@@ -314,6 +314,7 @@ export const hrTranslations: DefaultTranslationsObject = {
selectAll: 'Odaberite sve {{count}} {{label}}', selectAll: 'Odaberite sve {{count}} {{label}}',
selectAllRows: 'Odaberite sve redove', selectAllRows: 'Odaberite sve redove',
selectedCount: '{{count}} {{label}} odabrano', selectedCount: '{{count}} {{label}} odabrano',
selectLabel: 'Odaberite {{label}}',
selectValue: 'Odaberi vrijednost', selectValue: 'Odaberi vrijednost',
showAllLabel: 'Prikaži sve {{label}}', showAllLabel: 'Prikaži sve {{label}}',
sorryNotFound: 'Nažalost, ne postoji ništa što odgovara vašem zahtjevu.', sorryNotFound: 'Nažalost, ne postoji ništa što odgovara vašem zahtjevu.',
@@ -341,7 +342,9 @@ export const hrTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Nadolazeći događaji', upcomingEvents: 'Nadolazeći događaji',
updatedAt: 'Ažurirano u', updatedAt: 'Ažurirano u',
updatedCountSuccessfully: 'Uspješno ažurirano {{count}} {{label}}.', updatedCountSuccessfully: 'Uspješno ažurirano {{count}} {{label}}.',
updatedLabelSuccessfully: 'Uspješno ažurirano {{label}}.',
updatedSuccessfully: 'Uspješno ažurirano.', updatedSuccessfully: 'Uspješno ažurirano.',
updateForEveryone: 'Ažuriranje za sve',
updating: 'Ažuriranje', updating: 'Ažuriranje',
uploading: 'Prijenos', uploading: 'Prijenos',
uploadingBulk: 'Prenosim {{current}} od {{total}}', uploadingBulk: 'Prenosim {{current}} od {{total}}',

View File

@@ -317,6 +317,7 @@ export const huTranslations: DefaultTranslationsObject = {
selectAll: 'Az összes kijelölése: {{count}} {{label}}', selectAll: 'Az összes kijelölése: {{count}} {{label}}',
selectAllRows: 'Válassza ki az összes sort', selectAllRows: 'Válassza ki az összes sort',
selectedCount: '{{count}} {{label}} kiválasztva', selectedCount: '{{count}} {{label}} kiválasztva',
selectLabel: 'Válassza ki a(z) {{label}} opciót',
selectValue: 'Válasszon ki egy értéket', selectValue: 'Válasszon ki egy értéket',
showAllLabel: 'Mutasd az összes {{címke}}', showAllLabel: 'Mutasd az összes {{címke}}',
sorryNotFound: 'Sajnáljuk nincs semmi, ami megfelelne a kérésének.', sorryNotFound: 'Sajnáljuk nincs semmi, ami megfelelne a kérésének.',
@@ -344,7 +345,9 @@ export const huTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Közelgő események', upcomingEvents: 'Közelgő események',
updatedAt: 'Frissítve:', updatedAt: 'Frissítve:',
updatedCountSuccessfully: '{{count}} {{label}} sikeresen frissítve.', updatedCountSuccessfully: '{{count}} {{label}} sikeresen frissítve.',
updatedLabelSuccessfully: 'A(z) {{label}} sikeresen frissült.',
updatedSuccessfully: 'Sikeresen frissítve.', updatedSuccessfully: 'Sikeresen frissítve.',
updateForEveryone: 'Frissítés mindenkinek',
updating: 'Frissítés', updating: 'Frissítés',
uploading: 'Feltöltés', uploading: 'Feltöltés',
uploadingBulk: 'Feltöltés: {{current}} / {{total}}', uploadingBulk: 'Feltöltés: {{current}} / {{total}}',

View File

@@ -318,6 +318,7 @@ export const itTranslations: DefaultTranslationsObject = {
selectAll: 'Seleziona tutto {{count}} {{label}}', selectAll: 'Seleziona tutto {{count}} {{label}}',
selectAllRows: 'Seleziona tutte le righe', selectAllRows: 'Seleziona tutte le righe',
selectedCount: '{{count}} {{label}} selezionato', selectedCount: '{{count}} {{label}} selezionato',
selectLabel: 'Seleziona {{label}}',
selectValue: 'Seleziona un valore', selectValue: 'Seleziona un valore',
showAllLabel: 'Mostra tutti {{label}}', showAllLabel: 'Mostra tutti {{label}}',
sorryNotFound: "Siamo spiacenti, non c'è nulla che corrisponda alla tua richiesta.", sorryNotFound: "Siamo spiacenti, non c'è nulla che corrisponda alla tua richiesta.",
@@ -345,7 +346,9 @@ export const itTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Eventi Imminenti', upcomingEvents: 'Eventi Imminenti',
updatedAt: 'Aggiornato il', updatedAt: 'Aggiornato il',
updatedCountSuccessfully: '{{count}} {{label}} aggiornato con successo.', updatedCountSuccessfully: '{{count}} {{label}} aggiornato con successo.',
updatedLabelSuccessfully: '{{label}} aggiornata con successo.',
updatedSuccessfully: 'Aggiornato con successo.', updatedSuccessfully: 'Aggiornato con successo.',
updateForEveryone: 'Aggiornamento per tutti',
updating: 'Aggiornamento', updating: 'Aggiornamento',
uploading: 'Caricamento', uploading: 'Caricamento',
uploadingBulk: 'Caricamento {{current}} di {{total}}', uploadingBulk: 'Caricamento {{current}} di {{total}}',

View File

@@ -314,6 +314,7 @@ export const jaTranslations: DefaultTranslationsObject = {
selectAll: 'すべての{{count}}つの{{label}}を選択', selectAll: 'すべての{{count}}つの{{label}}を選択',
selectAllRows: 'すべての行を選択します', selectAllRows: 'すべての行を選択します',
selectedCount: '{{count}}つの{{label}}を選択中', selectedCount: '{{count}}つの{{label}}を選択中',
selectLabel: '{{label}}を選択してください',
selectValue: '値を選択', selectValue: '値を選択',
showAllLabel: 'すべての{{label}}を表示する', showAllLabel: 'すべての{{label}}を表示する',
sorryNotFound: '申し訳ありません。リクエストに対応する内容が見つかりませんでした。', sorryNotFound: '申し訳ありません。リクエストに対応する内容が見つかりませんでした。',
@@ -341,7 +342,9 @@ export const jaTranslations: DefaultTranslationsObject = {
upcomingEvents: '今後のイベント', upcomingEvents: '今後のイベント',
updatedAt: '更新日', updatedAt: '更新日',
updatedCountSuccessfully: '{{count}}つの{{label}}を正常に更新しました。', updatedCountSuccessfully: '{{count}}つの{{label}}を正常に更新しました。',
updatedLabelSuccessfully: '{{label}}の更新に成功しました。',
updatedSuccessfully: '更新成功。', updatedSuccessfully: '更新成功。',
updateForEveryone: '皆様への更新情報',
updating: '更新中', updating: '更新中',
uploading: 'アップロード中', uploading: 'アップロード中',
uploadingBulk: '{{current}} / {{total}} をアップロード中', uploadingBulk: '{{current}} / {{total}} をアップロード中',

View File

@@ -312,6 +312,7 @@ export const koTranslations: DefaultTranslationsObject = {
selectAll: '{{count}}개 {{label}} 모두 선택', selectAll: '{{count}}개 {{label}} 모두 선택',
selectAllRows: '모든 행 선택', selectAllRows: '모든 행 선택',
selectedCount: '{{count}}개의 {{label}} 선택됨', selectedCount: '{{count}}개의 {{label}} 선택됨',
selectLabel: '{{label}}을 선택하십시오.',
selectValue: '값 선택', selectValue: '값 선택',
showAllLabel: '{{label}} 모두 표시', showAllLabel: '{{label}} 모두 표시',
sorryNotFound: '죄송합니다. 요청과 일치하는 항목이 없습니다.', sorryNotFound: '죄송합니다. 요청과 일치하는 항목이 없습니다.',
@@ -339,7 +340,9 @@ export const koTranslations: DefaultTranslationsObject = {
upcomingEvents: '다가오는 이벤트', upcomingEvents: '다가오는 이벤트',
updatedAt: '업데이트 일시', updatedAt: '업데이트 일시',
updatedCountSuccessfully: '{{count}}개의 {{label}}을(를) 업데이트했습니다.', updatedCountSuccessfully: '{{count}}개의 {{label}}을(를) 업데이트했습니다.',
updatedLabelSuccessfully: '{{label}}이(가) 성공적으로 업데이트되었습니다.',
updatedSuccessfully: '성공적으로 업데이트되었습니다.', updatedSuccessfully: '성공적으로 업데이트되었습니다.',
updateForEveryone: '모두를 위한 업데이트',
updating: '업데이트 중', updating: '업데이트 중',
uploading: '업로드 중', uploading: '업로드 중',
uploadingBulk: '{{current}} / {{total}} 업로드 중', uploadingBulk: '{{current}} / {{total}} 업로드 중',

View File

@@ -316,6 +316,7 @@ export const ltTranslations: DefaultTranslationsObject = {
selectAll: 'Pasirinkite visus {{count}} {{label}}', selectAll: 'Pasirinkite visus {{count}} {{label}}',
selectAllRows: 'Pasirinkite visas eilutes', selectAllRows: 'Pasirinkite visas eilutes',
selectedCount: '{{count}} {{label}} pasirinkta', selectedCount: '{{count}} {{label}} pasirinkta',
selectLabel: 'Pasirinkite {{label}}',
selectValue: 'Pasirinkite reikšmę', selectValue: 'Pasirinkite reikšmę',
showAllLabel: 'Rodyti visus {{label}}', showAllLabel: 'Rodyti visus {{label}}',
sorryNotFound: 'Atsiprašau - nėra nieko, atitinkančio jūsų užklausą.', sorryNotFound: 'Atsiprašau - nėra nieko, atitinkančio jūsų užklausą.',
@@ -343,7 +344,9 @@ export const ltTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Artimieji renginiai', upcomingEvents: 'Artimieji renginiai',
updatedAt: 'Atnaujinta', updatedAt: 'Atnaujinta',
updatedCountSuccessfully: '{{count}} {{label}} sėkmingai atnaujinta.', updatedCountSuccessfully: '{{count}} {{label}} sėkmingai atnaujinta.',
updatedLabelSuccessfully: 'Sėkmingai atnaujinta {{label}}.',
updatedSuccessfully: 'Sėkmingai atnaujinta.', updatedSuccessfully: 'Sėkmingai atnaujinta.',
updateForEveryone: 'Atnaujinimas visiems',
updating: 'Atnaujinimas', updating: 'Atnaujinimas',
uploading: 'Įkeliama', uploading: 'Įkeliama',
uploadingBulk: 'Įkeliamas {{current}} iš {{total}}', uploadingBulk: 'Įkeliamas {{current}} iš {{total}}',

View File

@@ -317,6 +317,7 @@ export const myTranslations: DefaultTranslationsObject = {
selectAll: '{{count}} {{label}} အားလုံးကို ရွေးပါ', selectAll: '{{count}} {{label}} အားလုံးကို ရွေးပါ',
selectAllRows: 'အားလုံးကိုရွေးချယ်ပါ', selectAllRows: 'အားလုံးကိုရွေးချယ်ပါ',
selectedCount: '{{count}} {{label}} ကို ရွေးထားသည်။', selectedCount: '{{count}} {{label}} ကို ရွေးထားသည်။',
selectLabel: 'Pilih {{label}}',
selectValue: 'တစ်ခုခုကို ရွေးချယ်ပါ။', selectValue: 'တစ်ခုခုကို ရွေးချယ်ပါ။',
showAllLabel: 'Tunjukkan semua {{label}}', showAllLabel: 'Tunjukkan semua {{label}}',
sorryNotFound: 'ဝမ်းနည်းပါသည်။ သင်ရှာနေတဲ့ဟာ ဒီမှာမရှိပါ။', sorryNotFound: 'ဝမ်းနည်းပါသည်။ သင်ရှာနေတဲ့ဟာ ဒီမှာမရှိပါ။',
@@ -346,7 +347,9 @@ export const myTranslations: DefaultTranslationsObject = {
upcomingEvents: 'လာမည့် အစီအစဉ်များ', upcomingEvents: 'လာမည့် အစီအစဉ်များ',
updatedAt: 'ပြင်ဆင်ခဲ့သည့်အချိန်', updatedAt: 'ပြင်ဆင်ခဲ့သည့်အချိန်',
updatedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ခဲ့သည်။', updatedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ခဲ့သည်။',
updatedLabelSuccessfully: 'Berjaya mengemas kini {{label}}.',
updatedSuccessfully: 'အပ်ဒိတ်လုပ်ပြီးပါပြီ။', updatedSuccessfully: 'အပ်ဒိတ်လုပ်ပြီးပါပြီ။',
updateForEveryone: 'အားလုံးအတွက် အပြောင်းအလဲ',
updating: 'ပြင်ဆင်ရန်', updating: 'ပြင်ဆင်ရန်',
uploading: 'တင်ပေးနေသည်', uploading: 'တင်ပေးနေသည်',
uploadingBulk: 'တင်နေသည် {{current}} ခု အမှတ်ဖြစ်သည် {{total}} ခုစုစုပေါင်းဖြင့်', uploadingBulk: 'တင်နေသည် {{current}} ခု အမှတ်ဖြစ်သည် {{total}} ခုစုစုပေါင်းဖြင့်',

View File

@@ -315,6 +315,7 @@ export const nbTranslations: DefaultTranslationsObject = {
selectAll: 'Velg alle {{count}} {{label}}', selectAll: 'Velg alle {{count}} {{label}}',
selectAllRows: 'Velg alle rader', selectAllRows: 'Velg alle rader',
selectedCount: '{{count}} {{label}} valgt', selectedCount: '{{count}} {{label}} valgt',
selectLabel: 'Velg {{label}}',
selectValue: 'Velg en verdi', selectValue: 'Velg en verdi',
showAllLabel: 'Vis alle {{label}}', showAllLabel: 'Vis alle {{label}}',
sorryNotFound: 'Beklager, det er ingenting som samsvarer med forespørselen din.', sorryNotFound: 'Beklager, det er ingenting som samsvarer med forespørselen din.',
@@ -342,7 +343,9 @@ export const nbTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Kommende hendelser', upcomingEvents: 'Kommende hendelser',
updatedAt: 'Oppdatert', updatedAt: 'Oppdatert',
updatedCountSuccessfully: 'Oppdaterte {{count}} {{label}} vellykket.', updatedCountSuccessfully: 'Oppdaterte {{count}} {{label}} vellykket.',
updatedLabelSuccessfully: 'Oppdatert {{label}} vellykket.',
updatedSuccessfully: 'Oppdatert.', updatedSuccessfully: 'Oppdatert.',
updateForEveryone: 'Oppdatering for alle',
updating: 'Oppdatering', updating: 'Oppdatering',
uploading: 'Opplasting', uploading: 'Opplasting',
uploadingBulk: 'Laster opp {{current}} av {{total}}', uploadingBulk: 'Laster opp {{current}} av {{total}}',

View File

@@ -318,6 +318,7 @@ export const nlTranslations: DefaultTranslationsObject = {
selectAll: 'Alles selecteren {{count}} {{label}}', selectAll: 'Alles selecteren {{count}} {{label}}',
selectAllRows: 'Selecteer alle rijen', selectAllRows: 'Selecteer alle rijen',
selectedCount: '{{count}} {{label}} geselecteerd', selectedCount: '{{count}} {{label}} geselecteerd',
selectLabel: 'Selecteer {{label}}',
selectValue: 'Selecteer een waarde', selectValue: 'Selecteer een waarde',
showAllLabel: 'Toon alle {{label}}', showAllLabel: 'Toon alle {{label}}',
sorryNotFound: 'Sorry, er is niets dat overeen komt met uw verzoek.', sorryNotFound: 'Sorry, er is niets dat overeen komt met uw verzoek.',
@@ -345,7 +346,9 @@ export const nlTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Aankomende Evenementen', upcomingEvents: 'Aankomende Evenementen',
updatedAt: 'Aangepast op', updatedAt: 'Aangepast op',
updatedCountSuccessfully: '{{count}} {{label}} succesvol bijgewerkt.', updatedCountSuccessfully: '{{count}} {{label}} succesvol bijgewerkt.',
updatedLabelSuccessfully: 'Met succes {{label}} bijgewerkt.',
updatedSuccessfully: 'Succesvol aangepast.', updatedSuccessfully: 'Succesvol aangepast.',
updateForEveryone: 'Update voor iedereen',
updating: 'Bijwerken', updating: 'Bijwerken',
uploading: 'Uploaden', uploading: 'Uploaden',
uploadingBulk: 'Bezig met uploaden {{current}} van {{total}}', uploadingBulk: 'Bezig met uploaden {{current}} van {{total}}',

View File

@@ -314,6 +314,7 @@ export const plTranslations: DefaultTranslationsObject = {
selectAll: 'Wybierz wszystkie {{count}} {{label}}', selectAll: 'Wybierz wszystkie {{count}} {{label}}',
selectAllRows: 'Wybierz wszystkie wiersze', selectAllRows: 'Wybierz wszystkie wiersze',
selectedCount: 'Wybrano {{count}} {{label}}', selectedCount: 'Wybrano {{count}} {{label}}',
selectLabel: 'Wybierz {{label}}',
selectValue: 'Wybierz wartość', selectValue: 'Wybierz wartość',
showAllLabel: 'Pokaż wszystkie {{label}}', showAllLabel: 'Pokaż wszystkie {{label}}',
sorryNotFound: 'Przepraszamy — nie ma nic, co odpowiadałoby twojemu zapytaniu.', sorryNotFound: 'Przepraszamy — nie ma nic, co odpowiadałoby twojemu zapytaniu.',
@@ -341,7 +342,9 @@ export const plTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Nadchodzące Wydarzenia', upcomingEvents: 'Nadchodzące Wydarzenia',
updatedAt: 'Data edycji', updatedAt: 'Data edycji',
updatedCountSuccessfully: 'Pomyślnie zaktualizowano {{count}} {{label}}.', updatedCountSuccessfully: 'Pomyślnie zaktualizowano {{count}} {{label}}.',
updatedLabelSuccessfully: 'Pomyślnie zaktualizowano {{label}}.',
updatedSuccessfully: 'Aktualizacja zakończona sukcesem.', updatedSuccessfully: 'Aktualizacja zakończona sukcesem.',
updateForEveryone: 'Aktualizacja dla wszystkich',
updating: 'Aktualizacja', updating: 'Aktualizacja',
uploading: 'Przesyłanie', uploading: 'Przesyłanie',
uploadingBulk: 'Przesyłanie {{current}} z {{total}}', uploadingBulk: 'Przesyłanie {{current}} z {{total}}',

View File

@@ -315,6 +315,7 @@ export const ptTranslations: DefaultTranslationsObject = {
selectAll: 'Selecione tudo {{count}} {{label}}', selectAll: 'Selecione tudo {{count}} {{label}}',
selectAllRows: 'Selecione todas as linhas', selectAllRows: 'Selecione todas as linhas',
selectedCount: '{{count}} {{label}} selecionado', selectedCount: '{{count}} {{label}} selecionado',
selectLabel: 'Selecione {{label}}',
selectValue: 'Selecione um valor', selectValue: 'Selecione um valor',
showAllLabel: 'Mostre todos {{label}}', showAllLabel: 'Mostre todos {{label}}',
sorryNotFound: 'Desculpe—não há nada que corresponda à sua requisição.', sorryNotFound: 'Desculpe—não há nada que corresponda à sua requisição.',
@@ -342,7 +343,9 @@ export const ptTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Próximos Eventos', upcomingEvents: 'Próximos Eventos',
updatedAt: 'Atualizado Em', updatedAt: 'Atualizado Em',
updatedCountSuccessfully: 'Atualizado {{count}} {{label}} com sucesso.', updatedCountSuccessfully: 'Atualizado {{count}} {{label}} com sucesso.',
updatedLabelSuccessfully: '{{label}} atualizado com sucesso.',
updatedSuccessfully: 'Atualizado com sucesso.', updatedSuccessfully: 'Atualizado com sucesso.',
updateForEveryone: 'Atualização para todos',
updating: 'Atualizando', updating: 'Atualizando',
uploading: 'Fazendo upload', uploading: 'Fazendo upload',
uploadingBulk: 'Carregando {{current}} de {{total}}', uploadingBulk: 'Carregando {{current}} de {{total}}',

View File

@@ -318,6 +318,7 @@ export const roTranslations: DefaultTranslationsObject = {
selectAll: 'Selectați toate {{count}} {{label}}', selectAll: 'Selectați toate {{count}} {{label}}',
selectAllRows: 'Selectează toate rândurile', selectAllRows: 'Selectează toate rândurile',
selectedCount: '{{count}} {{label}} selectate', selectedCount: '{{count}} {{label}} selectate',
selectLabel: 'Selectați {{label}}',
selectValue: 'Selectați o valoare', selectValue: 'Selectați o valoare',
showAllLabel: 'Afișează toate {{eticheta}}', showAllLabel: 'Afișează toate {{eticheta}}',
sorryNotFound: 'Ne pare rău - nu există nimic care să corespundă cu cererea dvs.', sorryNotFound: 'Ne pare rău - nu există nimic care să corespundă cu cererea dvs.',
@@ -345,7 +346,9 @@ export const roTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Evenimente viitoare', upcomingEvents: 'Evenimente viitoare',
updatedAt: 'Actualizat la', updatedAt: 'Actualizat la',
updatedCountSuccessfully: 'Actualizate {{count}} {{label}} cu succes.', updatedCountSuccessfully: 'Actualizate {{count}} {{label}} cu succes.',
updatedLabelSuccessfully: '{{label}} actualizată cu succes.',
updatedSuccessfully: 'Actualizat cu succes.', updatedSuccessfully: 'Actualizat cu succes.',
updateForEveryone: 'Actualizare pentru toată lumea',
updating: 'Actualizare', updating: 'Actualizare',
uploading: 'Încărcare', uploading: 'Încărcare',
uploadingBulk: 'Încărcare {{current}} din {{total}}', uploadingBulk: 'Încărcare {{current}} din {{total}}',

View File

@@ -314,6 +314,7 @@ export const rsTranslations: DefaultTranslationsObject = {
selectAll: 'Одаберите све {{count}} {{label}}', selectAll: 'Одаберите све {{count}} {{label}}',
selectAllRows: 'Одаберите све редове', selectAllRows: 'Одаберите све редове',
selectedCount: '{{count}} {{label}} одабрано', selectedCount: '{{count}} {{label}} одабрано',
selectLabel: 'Izaberite {{label}}',
selectValue: 'Одабери вредност', selectValue: 'Одабери вредност',
showAllLabel: 'Прикажи све {{label}}', showAllLabel: 'Прикажи све {{label}}',
sorryNotFound: 'Нажалост, не постоји ништа што одговара вашем захтеву.', sorryNotFound: 'Нажалост, не постоји ништа што одговара вашем захтеву.',
@@ -341,7 +342,9 @@ export const rsTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Predstojeći događaji', upcomingEvents: 'Predstojeći događaji',
updatedAt: 'Ажурирано у', updatedAt: 'Ажурирано у',
updatedCountSuccessfully: 'Успешно ажурирано {{count}} {{label}}.', updatedCountSuccessfully: 'Успешно ажурирано {{count}} {{label}}.',
updatedLabelSuccessfully: 'Uspešno ažurirano {{label}}.',
updatedSuccessfully: 'Успешно ажурирано.', updatedSuccessfully: 'Успешно ажурирано.',
updateForEveryone: 'Ažuriranje za sve',
updating: 'Ажурирање', updating: 'Ажурирање',
uploading: 'Пренос', uploading: 'Пренос',
uploadingBulk: 'Отпремање {{current}} од {{total}}', uploadingBulk: 'Отпремање {{current}} од {{total}}',

View File

@@ -315,6 +315,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
selectAll: 'Odaberite sve {{count}} {{label}}', selectAll: 'Odaberite sve {{count}} {{label}}',
selectAllRows: 'Odaberite sve redove', selectAllRows: 'Odaberite sve redove',
selectedCount: '{{count}} {{label}} odabrano', selectedCount: '{{count}} {{label}} odabrano',
selectLabel: 'Izaberite {{label}}',
selectValue: 'Odaberi vrednost', selectValue: 'Odaberi vrednost',
showAllLabel: 'Prikaži sve {{label}}', showAllLabel: 'Prikaži sve {{label}}',
sorryNotFound: 'Nažalost, ne postoji ništa što odgovara vašem zahtevu.', sorryNotFound: 'Nažalost, ne postoji ništa što odgovara vašem zahtevu.',
@@ -342,7 +343,9 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Predstojeći događaji', upcomingEvents: 'Predstojeći događaji',
updatedAt: 'Ažurirano u', updatedAt: 'Ažurirano u',
updatedCountSuccessfully: 'Uspešno ažurirano {{count}} {{label}}.', updatedCountSuccessfully: 'Uspešno ažurirano {{count}} {{label}}.',
updatedLabelSuccessfully: 'Uspešno ažurirano {{label}}.',
updatedSuccessfully: 'Uspešno ažurirano.', updatedSuccessfully: 'Uspešno ažurirano.',
updateForEveryone: 'Ažuriranje za sve',
updating: 'Ažuriranje', updating: 'Ažuriranje',
uploading: 'Prenos', uploading: 'Prenos',
uploadingBulk: 'Otpremanje {{current}} od {{total}}', uploadingBulk: 'Otpremanje {{current}} od {{total}}',

View File

@@ -316,6 +316,7 @@ export const ruTranslations: DefaultTranslationsObject = {
selectAll: 'Выбрать все {{count}} {{label}}', selectAll: 'Выбрать все {{count}} {{label}}',
selectAllRows: 'Выбрать все строки', selectAllRows: 'Выбрать все строки',
selectedCount: '{{count}} {{label}} выбрано', selectedCount: '{{count}} {{label}} выбрано',
selectLabel: 'Выберите {{label}}',
selectValue: 'Выбрать значение', selectValue: 'Выбрать значение',
showAllLabel: 'Показать все {{label}}', showAllLabel: 'Показать все {{label}}',
sorryNotFound: 'К сожалению, ничего подходящего под ваш запрос нет.', sorryNotFound: 'К сожалению, ничего подходящего под ваш запрос нет.',
@@ -345,7 +346,9 @@ export const ruTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Предстоящие события', upcomingEvents: 'Предстоящие события',
updatedAt: 'Дата правки', updatedAt: 'Дата правки',
updatedCountSuccessfully: 'Обновлено {{count}} {{label}} успешно.', updatedCountSuccessfully: 'Обновлено {{count}} {{label}} успешно.',
updatedLabelSuccessfully: 'Успешно обновлено {{label}}.',
updatedSuccessfully: 'Успешно Обновлено.', updatedSuccessfully: 'Успешно Обновлено.',
updateForEveryone: 'Обновление для всех',
updating: 'Обновление', updating: 'Обновление',
uploading: 'Загрузка', uploading: 'Загрузка',
uploadingBulk: 'Загрузка {{current}} из {{total}}', uploadingBulk: 'Загрузка {{current}} из {{total}}',

View File

@@ -315,6 +315,7 @@ export const skTranslations: DefaultTranslationsObject = {
selectAll: 'Vybrať všetko {{count}} {{label}}', selectAll: 'Vybrať všetko {{count}} {{label}}',
selectAllRows: 'Vybrať všetky riadky', selectAllRows: 'Vybrať všetky riadky',
selectedCount: 'Vybrané {{count}} {{label}}', selectedCount: 'Vybrané {{count}} {{label}}',
selectLabel: 'Vyberte {{label}}',
selectValue: 'Vybrať hodnotu', selectValue: 'Vybrať hodnotu',
showAllLabel: 'Zobraziť všetky {{label}}', showAllLabel: 'Zobraziť všetky {{label}}',
sorryNotFound: 'Je nám ľúto, ale neexistuje nič, čo by zodpovedalo vášmu požiadavku.', sorryNotFound: 'Je nám ľúto, ale neexistuje nič, čo by zodpovedalo vášmu požiadavku.',
@@ -342,7 +343,9 @@ export const skTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Nadchádzajúce udalosti', upcomingEvents: 'Nadchádzajúce udalosti',
updatedAt: 'Aktualizované v', updatedAt: 'Aktualizované v',
updatedCountSuccessfully: 'Úspešne aktualizované {{count}} {{label}}.', updatedCountSuccessfully: 'Úspešne aktualizované {{count}} {{label}}.',
updatedLabelSuccessfully: 'Úspešne aktualizované {{label}}.',
updatedSuccessfully: 'Úspešne aktualizované.', updatedSuccessfully: 'Úspešne aktualizované.',
updateForEveryone: 'Aktualizácia pre všetkých',
updating: 'Aktualizácia', updating: 'Aktualizácia',
uploading: 'Nahrávanie', uploading: 'Nahrávanie',
uploadingBulk: 'Nahrávanie {{current}} z {{total}}', uploadingBulk: 'Nahrávanie {{current}} z {{total}}',

View File

@@ -313,6 +313,7 @@ export const slTranslations: DefaultTranslationsObject = {
selectAll: 'Izberi vse {{count}} {{label}}', selectAll: 'Izberi vse {{count}} {{label}}',
selectAllRows: 'Izberi vse vrstice', selectAllRows: 'Izberi vse vrstice',
selectedCount: '{{count}} {{label}} izbranih', selectedCount: '{{count}} {{label}} izbranih',
selectLabel: 'Izberite {{label}}',
selectValue: 'Izberi vrednost', selectValue: 'Izberi vrednost',
showAllLabel: 'Pokaži vse {{label}}', showAllLabel: 'Pokaži vse {{label}}',
sorryNotFound: 'Oprostite - ničesar ni mogoče najti, kar bi ustrezalo vaši zahtevi.', sorryNotFound: 'Oprostite - ničesar ni mogoče najti, kar bi ustrezalo vaši zahtevi.',
@@ -340,7 +341,9 @@ export const slTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Prihajajoči dogodki', upcomingEvents: 'Prihajajoči dogodki',
updatedAt: 'Posodobljeno', updatedAt: 'Posodobljeno',
updatedCountSuccessfully: 'Uspešno posodobljeno {{count}} {{label}}.', updatedCountSuccessfully: 'Uspešno posodobljeno {{count}} {{label}}.',
updatedLabelSuccessfully: '{{label}} uspešno posodobljen.',
updatedSuccessfully: 'Uspešno posodobljeno.', updatedSuccessfully: 'Uspešno posodobljeno.',
updateForEveryone: 'Posodobitev za vse',
updating: 'Posodabljanje', updating: 'Posodabljanje',
uploading: 'Nalaganje', uploading: 'Nalaganje',
uploadingBulk: 'Nalaganje {{current}} od {{total}}', uploadingBulk: 'Nalaganje {{current}} od {{total}}',

View File

@@ -315,6 +315,7 @@ export const svTranslations: DefaultTranslationsObject = {
selectAll: 'Välj alla {{count}} {{label}}', selectAll: 'Välj alla {{count}} {{label}}',
selectAllRows: 'Välj alla rader', selectAllRows: 'Välj alla rader',
selectedCount: '{{count}} {{label}} har valts', selectedCount: '{{count}} {{label}} har valts',
selectLabel: 'Välj {{label}}',
selectValue: 'Välj ett värde', selectValue: 'Välj ett värde',
showAllLabel: 'Visa alla {{label}}', showAllLabel: 'Visa alla {{label}}',
sorryNotFound: 'Tyvärrdet finns inget som motsvarar din begäran.', sorryNotFound: 'Tyvärrdet finns inget som motsvarar din begäran.',
@@ -342,7 +343,9 @@ export const svTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Kommande händelser', upcomingEvents: 'Kommande händelser',
updatedAt: 'Uppdaterat', updatedAt: 'Uppdaterat',
updatedCountSuccessfully: 'Uppdaterade {{count}} {{label}}', updatedCountSuccessfully: 'Uppdaterade {{count}} {{label}}',
updatedLabelSuccessfully: 'Uppdaterade {{label}} framgångsrikt.',
updatedSuccessfully: 'Uppdaterades', updatedSuccessfully: 'Uppdaterades',
updateForEveryone: 'Uppdatering för alla',
updating: 'Uppdaterar...', updating: 'Uppdaterar...',
uploading: 'Laddar upp...', uploading: 'Laddar upp...',
uploadingBulk: 'Laddar upp {{current}} av {{total}}', uploadingBulk: 'Laddar upp {{current}} av {{total}}',

View File

@@ -310,6 +310,7 @@ export const thTranslations: DefaultTranslationsObject = {
selectAll: 'เลือกทั้งหมด {{count}} {{label}}', selectAll: 'เลือกทั้งหมด {{count}} {{label}}',
selectAllRows: 'เลือกทุกแถว', selectAllRows: 'เลือกทุกแถว',
selectedCount: 'เลือก {{count}} {{label}} แล้ว', selectedCount: 'เลือก {{count}} {{label}} แล้ว',
selectLabel: 'เลือก {{label}}',
selectValue: 'เลือกค่า', selectValue: 'เลือกค่า',
showAllLabel: 'แสดง {{label}} ทั้งหมด', showAllLabel: 'แสดง {{label}} ทั้งหมด',
sorryNotFound: 'ขออภัย ไม่สามารถทำตามคำขอของคุณได้', sorryNotFound: 'ขออภัย ไม่สามารถทำตามคำขอของคุณได้',
@@ -337,7 +338,9 @@ export const thTranslations: DefaultTranslationsObject = {
upcomingEvents: 'กิจกรรมที่จะถึง', upcomingEvents: 'กิจกรรมที่จะถึง',
updatedAt: 'แก้ไขเมื่อ', updatedAt: 'แก้ไขเมื่อ',
updatedCountSuccessfully: 'อัปเดต {{count}} {{label}} เรียบร้อยแล้ว', updatedCountSuccessfully: 'อัปเดต {{count}} {{label}} เรียบร้อยแล้ว',
updatedLabelSuccessfully: 'อัปเดต {{label}} สำเร็จแล้ว',
updatedSuccessfully: 'แก้ไขสำเร็จ', updatedSuccessfully: 'แก้ไขสำเร็จ',
updateForEveryone: 'อัปเดตสำหรับทุกคน',
updating: 'กำลังอัปเดต', updating: 'กำลังอัปเดต',
uploading: 'กำลังอัปโหลด', uploading: 'กำลังอัปโหลด',
uploadingBulk: 'อัปโหลด {{current}} จาก {{total}}', uploadingBulk: 'อัปโหลด {{current}} จาก {{total}}',

View File

@@ -318,6 +318,7 @@ export const trTranslations: DefaultTranslationsObject = {
selectAll: "Tüm {{count}} {{label}}'ı seçin", selectAll: "Tüm {{count}} {{label}}'ı seçin",
selectAllRows: 'Tüm satırları seçin', selectAllRows: 'Tüm satırları seçin',
selectedCount: '{{count}} {{label}} seçildi', selectedCount: '{{count}} {{label}} seçildi',
selectLabel: '{{label}} seçin',
selectValue: 'Bir değer seçin', selectValue: 'Bir değer seçin',
showAllLabel: 'Tüm {{label}} göster', showAllLabel: 'Tüm {{label}} göster',
sorryNotFound: 'Üzgünüz, isteğinizle eşleşen bir sonuç bulunamadı.', sorryNotFound: 'Üzgünüz, isteğinizle eşleşen bir sonuç bulunamadı.',
@@ -346,7 +347,9 @@ export const trTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Yaklaşan Etkinlikler', upcomingEvents: 'Yaklaşan Etkinlikler',
updatedAt: 'Güncellenme tarihi', updatedAt: 'Güncellenme tarihi',
updatedCountSuccessfully: '{{count}} {{label}} başarıyla güncellendi.', updatedCountSuccessfully: '{{count}} {{label}} başarıyla güncellendi.',
updatedLabelSuccessfully: '{{label}} başarıyla güncellendi.',
updatedSuccessfully: 'Başarıyla güncellendi.', updatedSuccessfully: 'Başarıyla güncellendi.',
updateForEveryone: 'Herkes için güncelleme',
updating: 'Güncelleniyor', updating: 'Güncelleniyor',
uploading: 'Yükleniyor', uploading: 'Yükleniyor',
uploadingBulk: "{{total}}'den {{current}} yükleniyor", uploadingBulk: "{{total}}'den {{current}} yükleniyor",

View File

@@ -313,6 +313,7 @@ export const ukTranslations: DefaultTranslationsObject = {
selectAll: 'Вибрати всі {{count}} {{label}}', selectAll: 'Вибрати всі {{count}} {{label}}',
selectAllRows: 'Обрати всі рядки', selectAllRows: 'Обрати всі рядки',
selectedCount: 'Обрано {{count}} {{label}}', selectedCount: 'Обрано {{count}} {{label}}',
selectLabel: 'Виберіть {{label}}',
selectValue: 'Обрати значення', selectValue: 'Обрати значення',
showAllLabel: 'Показати всі {{label}}', showAllLabel: 'Показати всі {{label}}',
sorryNotFound: 'Вибачте, немає нічого, що відповідало б Вашому запиту.', sorryNotFound: 'Вибачте, немає нічого, що відповідало б Вашому запиту.',
@@ -340,7 +341,9 @@ export const ukTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Майбутні події', upcomingEvents: 'Майбутні події',
updatedAt: 'Змінено', updatedAt: 'Змінено',
updatedCountSuccessfully: 'Успішно оновлено {{count}} {{label}}.', updatedCountSuccessfully: 'Успішно оновлено {{count}} {{label}}.',
updatedLabelSuccessfully: 'Успішно оновлено {{label}}.',
updatedSuccessfully: 'Успішно відредаговано.', updatedSuccessfully: 'Успішно відредаговано.',
updateForEveryone: 'Оновлення для всіх',
updating: 'оновлення', updating: 'оновлення',
uploading: 'завантаження', uploading: 'завантаження',
uploadingBulk: 'Завантаження {{current}} з {{total}}', uploadingBulk: 'Завантаження {{current}} з {{total}}',

View File

@@ -313,6 +313,7 @@ export const viTranslations: DefaultTranslationsObject = {
selectAll: 'Chọn tất cả {{count}} {{label}}', selectAll: 'Chọn tất cả {{count}} {{label}}',
selectAllRows: 'Chọn tất cả các hàng', selectAllRows: 'Chọn tất cả các hàng',
selectedCount: 'Đã chọn {{count}} {{label}}', selectedCount: 'Đã chọn {{count}} {{label}}',
selectLabel: 'Chọn {{label}}',
selectValue: 'Chọn một giá trị', selectValue: 'Chọn một giá trị',
showAllLabel: 'Hiển thị tất cả {{label}}', showAllLabel: 'Hiển thị tất cả {{label}}',
sorryNotFound: 'Xin lỗi, không có kết quả nào tương ứng với request của bạn.', sorryNotFound: 'Xin lỗi, không có kết quả nào tương ứng với request của bạn.',
@@ -340,7 +341,9 @@ export const viTranslations: DefaultTranslationsObject = {
upcomingEvents: 'Sự kiện sắp tới', upcomingEvents: 'Sự kiện sắp tới',
updatedAt: 'Ngày cập nhật', updatedAt: 'Ngày cập nhật',
updatedCountSuccessfully: 'Đã cập nhật thành công {{count}} {{label}}.', updatedCountSuccessfully: 'Đã cập nhật thành công {{count}} {{label}}.',
updatedLabelSuccessfully: 'Đã cập nhật {{label}} thành công.',
updatedSuccessfully: 'Cập nhật thành công.', updatedSuccessfully: 'Cập nhật thành công.',
updateForEveryone: 'Cập nhật cho mọi người',
updating: 'Đang cập nhật', updating: 'Đang cập nhật',
uploading: 'Đang tải lên', uploading: 'Đang tải lên',
uploadingBulk: 'Đang tải lên {{current}} trong tổng số {{total}}', uploadingBulk: 'Đang tải lên {{current}} trong tổng số {{total}}',

View File

@@ -303,6 +303,7 @@ export const zhTranslations: DefaultTranslationsObject = {
selectAll: '选择所有 {{count}} {{label}}', selectAll: '选择所有 {{count}} {{label}}',
selectAllRows: '选择所有行', selectAllRows: '选择所有行',
selectedCount: '已选择 {{count}} {{label}}', selectedCount: '已选择 {{count}} {{label}}',
selectLabel: '选择{{label}}',
selectValue: '选择一个值', selectValue: '选择一个值',
showAllLabel: '显示所有{{label}}', showAllLabel: '显示所有{{label}}',
sorryNotFound: '对不起,没有与您的请求相对应的东西。', sorryNotFound: '对不起,没有与您的请求相对应的东西。',
@@ -330,7 +331,9 @@ export const zhTranslations: DefaultTranslationsObject = {
upcomingEvents: '即将到来的活动', upcomingEvents: '即将到来的活动',
updatedAt: '更新于', updatedAt: '更新于',
updatedCountSuccessfully: '已成功更新 {{count}} {{label}}。', updatedCountSuccessfully: '已成功更新 {{count}} {{label}}。',
updatedLabelSuccessfully: '成功更新了 {{label}}。',
updatedSuccessfully: '更新成功。', updatedSuccessfully: '更新成功。',
updateForEveryone: '给大家的更新',
updating: '更新中', updating: '更新中',
uploading: '上传中', uploading: '上传中',
uploadingBulk: '正在上传{{current}},共{{total}}', uploadingBulk: '正在上传{{current}},共{{total}}',

View File

@@ -303,6 +303,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
selectAll: '選擇所有 {{count}} 個 {{label}}', selectAll: '選擇所有 {{count}} 個 {{label}}',
selectAllRows: '選擇所有行', selectAllRows: '選擇所有行',
selectedCount: '已選擇 {{count}} 個 {{label}}', selectedCount: '已選擇 {{count}} 個 {{label}}',
selectLabel: '選擇 {{label}}',
selectValue: '選擇一個值', selectValue: '選擇一個值',
showAllLabel: '顯示所有{{label}}', showAllLabel: '顯示所有{{label}}',
sorryNotFound: '對不起,沒有找到您請求的東西。', sorryNotFound: '對不起,沒有找到您請求的東西。',
@@ -330,7 +331,9 @@ export const zhTwTranslations: DefaultTranslationsObject = {
upcomingEvents: '即將來臨的活動', upcomingEvents: '即將來臨的活動',
updatedAt: '更新於', updatedAt: '更新於',
updatedCountSuccessfully: '已成功更新 {{count}} 個 {{label}}。', updatedCountSuccessfully: '已成功更新 {{count}} 個 {{label}}。',
updatedLabelSuccessfully: '成功更新了{{label}}。',
updatedSuccessfully: '更新成功。', updatedSuccessfully: '更新成功。',
updateForEveryone: '給所有人的更新',
updating: '更新中', updating: '更新中',
uploading: '上傳中', uploading: '上傳中',
uploadingBulk: '正在上傳 {{current}} / {{total}}', uploadingBulk: '正在上傳 {{current}} / {{total}}',

View File

@@ -5,11 +5,10 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
background: var(--theme-elevation-50); background: var(--theme-elevation-50);
padding: base(1) base(1) base(0.5); padding: var(--base);
gap: calc(var(--base) / 2);
&__column { &__column {
margin-right: base(0.5);
margin-bottom: base(0.5);
background-color: transparent; background-color: transparent;
box-shadow: 0 0 0 1px var(--theme-elevation-150); box-shadow: 0 0 0 1px var(--theme-elevation-150);

View File

@@ -8,9 +8,9 @@ import { FieldLabel } from '../../fields/FieldLabel/index.js'
import { PlusIcon } from '../../icons/Plus/index.js' import { PlusIcon } from '../../icons/Plus/index.js'
import { XIcon } from '../../icons/X/index.js' import { XIcon } from '../../icons/X/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js' import { useEditDepth } from '../../providers/EditDepth/index.js'
import { useTableColumns } from '../../providers/TableColumns/index.js'
import { DraggableSortable } from '../DraggableSortable/index.js' import { DraggableSortable } from '../DraggableSortable/index.js'
import { Pill } from '../Pill/index.js' import { Pill } from '../Pill/index.js'
import { useTableColumns } from '../TableColumns/index.js'
import './index.scss' import './index.scss'
const baseClass = 'column-selector' const baseClass = 'column-selector'

View File

@@ -60,8 +60,6 @@ export const DeleteDocument: React.FC<Props> = (props) => {
const { startRouteTransition } = useRouteTransition() const { startRouteTransition } = useRouteTransition()
const { openModal } = useModal() const { openModal } = useModal()
const titleToRender = titleFromProps || title || id
const modalSlug = `delete-${id}` const modalSlug = `delete-${id}`
const addDefaultError = useCallback(() => { const addDefaultError = useCallback(() => {
@@ -163,7 +161,7 @@ export const DeleteDocument: React.FC<Props> = (props) => {
t={t} t={t}
variables={{ variables={{
label: getTranslation(singularLabel, i18n), label: getTranslation(singularLabel, i18n),
title: titleToRender, title: titleFromProps || title || id,
}} }}
/> />
} }

View File

@@ -4,6 +4,7 @@ import type { ClientCollectionConfig } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter, useSearchParams } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import { mergeListSearchAndWhere } from 'payload/shared'
import * as qs from 'qs-esm' import * as qs from 'qs-esm'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -14,7 +15,6 @@ import { useRouteCache } from '../../providers/RouteCache/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js' import { ConfirmationModal } from '../ConfirmationModal/index.js'
import './index.scss' import './index.scss'

View File

@@ -22,11 +22,11 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
drawerSlug, drawerSlug,
Header, Header,
initialData, initialData,
initialState,
onDelete: onDeleteFromProps, onDelete: onDeleteFromProps,
onDuplicate: onDuplicateFromProps, onDuplicate: onDuplicateFromProps,
onSave: onSaveFromProps, onSave: onSaveFromProps,
overrideEntityVisibility = true, overrideEntityVisibility = true,
redirectAfterCreate,
redirectAfterDelete, redirectAfterDelete,
redirectAfterDuplicate, redirectAfterDuplicate,
}) => { }) => {
@@ -62,6 +62,7 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
initialData, initialData,
locale, locale,
overrideEntityVisibility, overrideEntityVisibility,
redirectAfterCreate,
redirectAfterDelete: redirectAfterDelete !== undefined ? redirectAfterDelete : false, redirectAfterDelete: redirectAfterDelete !== undefined ? redirectAfterDelete : false,
redirectAfterDuplicate: redirectAfterDuplicate:
redirectAfterDuplicate !== undefined ? redirectAfterDuplicate : false, redirectAfterDuplicate !== undefined ? redirectAfterDuplicate : false,

View File

@@ -12,8 +12,12 @@ export type DocumentDrawerProps = {
readonly drawerSlug?: string readonly drawerSlug?: string
readonly id?: null | number | string readonly id?: null | number | string
readonly initialData?: Data readonly initialData?: Data
/**
* @deprecated
*/
readonly initialState?: FormState readonly initialState?: FormState
readonly overrideEntityVisibility?: boolean readonly overrideEntityVisibility?: boolean
readonly redirectAfterCreate?: boolean
readonly redirectAfterDelete?: boolean readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean readonly redirectAfterDuplicate?: boolean
} & Pick<DocumentDrawerContextProps, 'onDelete' | 'onDuplicate' | 'onSave'> & } & Pick<DocumentDrawerContextProps, 'onDelete' | 'onDuplicate' | 'onSave'> &

View File

@@ -5,7 +5,7 @@ import type { SelectType } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter, useSearchParams } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import { unflatten } from 'payload/shared' import { mergeListSearchAndWhere, unflatten } from 'payload/shared'
import * as qs from 'qs-esm' import * as qs from 'qs-esm'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
@@ -27,7 +27,6 @@ import { SelectAllStatus, useSelection } from '../../providers/Selection/index.j
import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js'
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { FieldSelect } from '../FieldSelect/index.js' import { FieldSelect } from '../FieldSelect/index.js'
import { baseClass, type EditManyProps } from './index.js' import { baseClass, type EditManyProps } from './index.js'

View File

@@ -0,0 +1,21 @@
@import '../../../scss/styles';
@layer payload-default {
.active-query-preset {
&.pill {
max-width: 150px;
overflow: hidden;
}
.pill__label {
display: flex;
align-items: center;
}
&__label-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}

View File

@@ -0,0 +1,65 @@
'use client'
import type { QueryPreset } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { PeopleIcon } from '../../../icons/People/index.js'
import { XIcon } from '../../../icons/X/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { Pill } from '../../Pill/index.js'
import './index.scss'
const baseClass = 'active-query-preset'
export function ActiveQueryPreset({
activePreset,
openPresetListDrawer,
resetPreset,
}: {
activePreset: QueryPreset
openPresetListDrawer: () => void
resetPreset: () => Promise<void>
}) {
const { i18n, t } = useTranslation()
const { getEntityConfig } = useConfig()
const presetsConfig = getEntityConfig({
collectionSlug: 'payload-query-presets',
})
return (
<Pill
className={[baseClass, activePreset && `${baseClass}--active`].filter(Boolean).join(' ')}
id="select-preset"
onClick={() => {
openPresetListDrawer()
}}
pillStyle={activePreset ? 'always-white' : 'light'}
>
{activePreset?.isShared && <PeopleIcon className={`${baseClass}__shared`} />}
<div className={`${baseClass}__label-text`}>
{activePreset?.title ||
t('general:selectLabel', { label: getTranslation(presetsConfig.labels.singular, i18n) })}
</div>
{activePreset ? (
<div
className={`${baseClass}__clear`}
id="clear-preset"
onClick={async (e) => {
e.stopPropagation()
await resetPreset()
}}
onKeyDown={async (e) => {
e.stopPropagation()
await resetPreset()
}}
role="button"
tabIndex={0}
>
<XIcon />
</div>
) : null}
</Pill>
)
}

View File

@@ -24,18 +24,14 @@
border-radius: 0; border-radius: 0;
} }
&__modified {
color: var(--theme-elevation-500);
}
&__buttons-wrap { &__buttons-wrap {
display: flex; display: flex;
align-items: center; align-items: center;
gap: base(0.2); gap: 4px;
.pill {
background-color: var(--theme-elevation-150);
&:hover {
background-color: var(--theme-elevation-100);
}
}
} }
.column-selector, .column-selector,

View File

@@ -1,9 +1,11 @@
'use client' 'use client'
import type { ClientCollectionConfig, ResolvedFilterOptions, Where } from 'payload'
import { useWindowInfo } from '@faceless-ui/window-info' import { useWindowInfo } from '@faceless-ui/window-info'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import React, { useEffect, useRef, useState } from 'react' import { validateWhereQuery } from 'payload/shared'
import React, { Fragment, useEffect, useRef, useState } from 'react'
import type { ListControlsProps } from './types.js'
import { Popup, PopupList } from '../../elements/Popup/index.js' import { Popup, PopupList } from '../../elements/Popup/index.js'
import { useUseTitleField } from '../../hooks/useUseAsTitle.js' import { useUseTitleField } from '../../hooks/useUseAsTitle.js'
@@ -17,36 +19,13 @@ import { ColumnSelector } from '../ColumnSelector/index.js'
import { Pill } from '../Pill/index.js' import { Pill } from '../Pill/index.js'
import { SearchFilter } from '../SearchFilter/index.js' import { SearchFilter } from '../SearchFilter/index.js'
import { WhereBuilder } from '../WhereBuilder/index.js' import { WhereBuilder } from '../WhereBuilder/index.js'
import validateWhereQuery from '../WhereBuilder/validateWhereQuery.js' import { ActiveQueryPreset } from './ActiveQueryPreset/index.js'
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js' import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched.js'
import { useQueryPresets } from './useQueryPresets.js'
import './index.scss' import './index.scss'
const baseClass = 'list-controls' const baseClass = 'list-controls'
export type ListControlsProps = {
readonly beforeActions?: React.ReactNode[]
readonly collectionConfig: ClientCollectionConfig
readonly collectionSlug: string
/**
* @deprecated
* These are now handled by the `ListSelection` component
*/
readonly disableBulkDelete?: boolean
/**
* @deprecated
* These are now handled by the `ListSelection` component
*/
readonly disableBulkEdit?: boolean
readonly enableColumns?: boolean
readonly enableSort?: boolean
readonly handleSearchChange?: (search: string) => void
readonly handleSortChange?: (sort: string) => void
readonly handleWhereChange?: (where: Where) => void
readonly listMenuItems?: React.ReactNode[]
readonly renderedFilters?: Map<string, React.ReactNode>
readonly resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
}
/** /**
* The ListControls component is used to render the controls (search, filter, where) * The ListControls component is used to render the controls (search, filter, where)
* for a collection's list view. You can find those directly above the table which lists * for a collection's list view. You can find those directly above the table which lists
@@ -57,15 +36,36 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
beforeActions, beforeActions,
collectionConfig, collectionConfig,
collectionSlug, collectionSlug,
disableQueryPresets,
enableColumns = true, enableColumns = true,
enableSort = false, enableSort = false,
listMenuItems, listMenuItems: listMenuItemsFromProps,
queryPreset: activePreset,
queryPresetPermissions,
renderedFilters, renderedFilters,
resolvedFilterOptions, resolvedFilterOptions,
} = props } = props
const { handleSearchChange, query } = useListQuery() const { handleSearchChange, query } = useListQuery()
const {
CreateNewPresetDrawer,
DeletePresetModal,
EditPresetDrawer,
hasModifiedPreset,
openPresetListDrawer,
PresetListDrawer,
queryPresetMenuItems,
resetPreset,
} = useQueryPresets({
activePreset,
collectionSlug,
queryPresetPermissions,
})
const titleField = useUseTitleField(collectionConfig) const titleField = useUseTitleField(collectionConfig)
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const { const {
breakpoints: { s: smallBreak }, breakpoints: { s: smallBreak },
} = useWindowInfo() } = useWindowInfo()
@@ -135,109 +135,131 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
} }
}, [t, listSearchableFields, i18n, searchLabel]) }, [t, listSearchableFields, i18n, searchLabel])
let listMenuItems: React.ReactNode[] = listMenuItemsFromProps
if (
collectionConfig?.enableQueryPresets &&
!disableQueryPresets &&
queryPresetMenuItems?.length > 0
) {
// Cannot push or unshift into `listMenuItemsFromProps` as it will mutate the original array
listMenuItems = [
...queryPresetMenuItems,
listMenuItemsFromProps?.length > 0 ? <PopupList.Divider key="divider" /> : null,
...(listMenuItemsFromProps || []),
]
}
return ( return (
<div className={baseClass}> <Fragment>
<div className={`${baseClass}__wrap`}> <div className={baseClass}>
<SearchIcon /> <div className={`${baseClass}__wrap`}>
<SearchFilter <SearchIcon />
fieldName={titleField && 'name' in titleField ? titleField?.name : null} <SearchFilter
handleChange={(search) => { fieldName={titleField && 'name' in titleField ? titleField?.name : null}
return void handleSearchChange(search) handleChange={(search) => {
}} return void handleSearchChange(search)
// @ts-expect-error @todo: fix types }}
initialParams={query} // @ts-expect-error @todo: fix types
key={collectionSlug} initialParams={query}
label={searchLabelTranslated.current} key={collectionSlug}
/> label={searchLabelTranslated.current}
<div className={`${baseClass}__buttons`}> />
<div className={`${baseClass}__buttons-wrap`}> {activePreset && hasModifiedPreset ? (
{!smallBreak && <React.Fragment>{beforeActions && beforeActions}</React.Fragment>} <div className={`${baseClass}__modified`}>Modified</div>
{enableColumns && ( ) : null}
<div className={`${baseClass}__buttons`}>
<div className={`${baseClass}__buttons-wrap`}>
{!smallBreak && <React.Fragment>{beforeActions && beforeActions}</React.Fragment>}
{enableColumns && (
<Pill
aria-controls={`${baseClass}-columns`}
aria-expanded={visibleDrawer === 'columns'}
className={`${baseClass}__toggle-columns`}
icon={<ChevronIcon direction={visibleDrawer === 'columns' ? 'up' : 'down'} />}
onClick={() =>
setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)
}
pillStyle="light"
>
{t('general:columns')}
</Pill>
)}
<Pill <Pill
aria-controls={`${baseClass}-columns`} aria-controls={`${baseClass}-where`}
aria-expanded={visibleDrawer === 'columns'} aria-expanded={visibleDrawer === 'where'}
className={`${baseClass}__toggle-columns`} className={`${baseClass}__toggle-where`}
icon={<ChevronIcon direction={visibleDrawer === 'columns' ? 'up' : 'down'} />} icon={<ChevronIcon direction={visibleDrawer === 'where' ? 'up' : 'down'} />}
onClick={() => onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)}
setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)
}
pillStyle="light" pillStyle="light"
> >
{t('general:columns')} {t('general:filters')}
</Pill> </Pill>
)} {enableSort && (
<Pill <Pill
aria-controls={`${baseClass}-where`} aria-controls={`${baseClass}-sort`}
aria-expanded={visibleDrawer === 'where'} aria-expanded={visibleDrawer === 'sort'}
className={`${baseClass}__toggle-where`} className={`${baseClass}__toggle-sort`}
icon={<ChevronIcon direction={visibleDrawer === 'where' ? 'up' : 'down'} />} icon={<ChevronIcon />}
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)} onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
pillStyle="light" pillStyle="light"
> >
{t('general:filters')} {t('general:sort')}
</Pill> </Pill>
{enableSort && ( )}
<Pill {!disableQueryPresets && (
aria-controls={`${baseClass}-sort`} <ActiveQueryPreset
aria-expanded={visibleDrawer === 'sort'} activePreset={activePreset}
className={`${baseClass}__toggle-sort`} openPresetListDrawer={openPresetListDrawer}
icon={<ChevronIcon />} resetPreset={resetPreset}
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)} />
pillStyle="light" )}
> {listMenuItems && Array.isArray(listMenuItems) && listMenuItems.length > 0 && (
{t('general:sort')} <Popup
</Pill> button={<Dots ariaLabel={t('general:moreOptions')} />}
)} className={`${baseClass}__popup`}
{listMenuItems && ( horizontalAlign="right"
<Popup id="list-menu"
button={<Dots ariaLabel={t('general:moreOptions')} />} size="large"
className={`${baseClass}__popup`} verticalAlign="bottom"
horizontalAlign="right" >
size="large" <PopupList.ButtonGroup>
verticalAlign="bottom" {listMenuItems.map((item, i) => (
> <Fragment key={`list-menu-item-${i}`}>{item}</Fragment>
<PopupList.ButtonGroup>{listMenuItems.map((item) => item)}</PopupList.ButtonGroup> ))}
</Popup> </PopupList.ButtonGroup>
)} </Popup>
)}
</div>
</div> </div>
</div> </div>
{enableColumns && (
<AnimateHeight
className={`${baseClass}__columns`}
height={visibleDrawer === 'columns' ? 'auto' : 0}
id={`${baseClass}-columns`}
>
<ColumnSelector collectionSlug={collectionConfig.slug} />
</AnimateHeight>
)}
<AnimateHeight
className={`${baseClass}__where`}
height={visibleDrawer === 'where' ? 'auto' : 0}
id={`${baseClass}-where`}
>
<WhereBuilder
collectionPluralLabel={collectionConfig?.labels?.plural}
collectionSlug={collectionConfig.slug}
fields={collectionConfig?.fields}
renderedFilters={renderedFilters}
resolvedFilterOptions={resolvedFilterOptions}
/>
</AnimateHeight>
</div> </div>
{enableColumns && ( {PresetListDrawer}
<AnimateHeight {EditPresetDrawer}
className={`${baseClass}__columns`} {CreateNewPresetDrawer}
height={visibleDrawer === 'columns' ? 'auto' : 0} {DeletePresetModal}
id={`${baseClass}-columns`} </Fragment>
>
<ColumnSelector collectionSlug={collectionConfig.slug} />
</AnimateHeight>
)}
<AnimateHeight
className={`${baseClass}__where`}
height={visibleDrawer === 'where' ? 'auto' : 0}
id={`${baseClass}-where`}
>
<WhereBuilder
collectionPluralLabel={collectionConfig?.labels?.plural}
collectionSlug={collectionConfig.slug}
fields={collectionConfig?.fields}
renderedFilters={renderedFilters}
resolvedFilterOptions={resolvedFilterOptions}
/>
</AnimateHeight>
{enableSort && (
<AnimateHeight
className={`${baseClass}__sort`}
height={visibleDrawer === 'sort' ? 'auto' : 0}
id={`${baseClass}-sort`}
>
<p>Sort Complex</p>
{/* <SortComplex
collection={collection}
handleChange={handleSortChange}
modifySearchQuery={modifySearchQuery}
/> */}
</AnimateHeight>
)}
</div>
) )
} }

View File

@@ -0,0 +1,34 @@
import type {
ClientCollectionConfig,
QueryPreset,
ResolvedFilterOptions,
SanitizedCollectionPermission,
Where,
} from 'payload'
export type ListControlsProps = {
readonly beforeActions?: React.ReactNode[]
readonly collectionConfig: ClientCollectionConfig
readonly collectionSlug: string
/**
* @deprecated
* These are now handled by the `ListSelection` component
*/
readonly disableBulkDelete?: boolean
/**
* @deprecated
* These are now handled by the `ListSelection` component
*/
readonly disableBulkEdit?: boolean
readonly disableQueryPresets?: boolean
readonly enableColumns?: boolean
readonly enableSort?: boolean
readonly handleSearchChange?: (search: string) => void
readonly handleSortChange?: (sort: string) => void
readonly handleWhereChange?: (where: Where) => void
readonly listMenuItems?: React.ReactNode[]
readonly queryPreset?: QueryPreset
readonly queryPresetPermissions?: SanitizedCollectionPermission
readonly renderedFilters?: Map<string, React.ReactNode>
readonly resolvedFilterOptions?: Map<string, ResolvedFilterOptions>
}

View File

@@ -0,0 +1,326 @@
import type { CollectionSlug, QueryPreset, SanitizedCollectionPermission } from 'payload'
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import { transformColumnsToPreferences, transformColumnsToSearchParams } from 'payload/shared'
import React, { Fragment, useCallback, useMemo } from 'react'
import { toast } from 'sonner'
import { useConfig } from '../../providers/Config/index.js'
import { useListQuery } from '../../providers/ListQuery/context.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js'
import { useDocumentDrawer } from '../DocumentDrawer/index.js'
import { useListDrawer } from '../ListDrawer/index.js'
import { PopupList } from '../Popup/index.js'
import { PopupListGroupLabel } from '../Popup/PopupGroupLabel/index.js'
import { Translation } from '../Translation/index.js'
const confirmDeletePresetModalSlug = 'confirm-delete-preset'
const queryPresetsSlug = 'payload-query-presets'
export const useQueryPresets = ({
activePreset,
collectionSlug,
queryPresetPermissions,
}: {
activePreset: QueryPreset
collectionSlug: CollectionSlug
queryPresetPermissions: SanitizedCollectionPermission
}): {
CreateNewPresetDrawer: React.ReactNode
DeletePresetModal: React.ReactNode
EditPresetDrawer: React.ReactNode
hasModifiedPreset: boolean
openPresetListDrawer: () => void
PresetListDrawer: React.ReactNode
queryPresetMenuItems: React.ReactNode[]
resetPreset: () => Promise<void>
} => {
const { modified, query, refineListData, setModified: setQueryModified } = useListQuery()
const { i18n, t } = useTranslation()
const { openModal } = useModal()
const {
config: {
routes: { api: apiRoute },
},
getEntityConfig,
} = useConfig()
const presetConfig = getEntityConfig({ collectionSlug: queryPresetsSlug })
const [PresetDocumentDrawer, , { openDrawer: openDocumentDrawer }] = useDocumentDrawer({
id: activePreset?.id,
collectionSlug: queryPresetsSlug,
})
const [
CreateNewPresetDrawer,
,
{ closeDrawer: closeCreateNewDrawer, openDrawer: openCreateNewDrawer },
] = useDocumentDrawer({
collectionSlug: queryPresetsSlug,
})
const [ListDrawer, , { closeDrawer: closeListDrawer, openDrawer: openListDrawer }] =
useListDrawer({
collectionSlugs: [queryPresetsSlug],
})
const handlePresetChange = useCallback(
async (preset: QueryPreset) => {
await refineListData(
{
columns: preset.columns ? transformColumnsToSearchParams(preset.columns) : undefined,
preset: preset.id,
where: preset.where,
},
false,
)
},
[refineListData],
)
const resetQueryPreset = useCallback(async () => {
await refineListData(
{
columns: undefined,
preset: undefined,
where: undefined,
},
false,
)
}, [refineListData])
const handleDeletePreset = useCallback(async () => {
try {
await fetch(`${apiRoute}/${queryPresetsSlug}/${activePreset.id}`, {
method: 'DELETE',
}).then(async (res) => {
try {
const json = await res.json()
if (res.status < 400) {
toast.success(
t('general:titleDeleted', {
label: getTranslation(presetConfig?.labels?.singular, i18n),
title: activePreset.title,
}),
)
await resetQueryPreset()
} else {
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
toast.error(t('error:deletingTitle', { title: activePreset.title }))
}
}
} catch (_err) {
toast.error(t('error:deletingTitle', { title: activePreset.title }))
}
})
} catch (_err) {
toast.error(t('error:deletingTitle', { title: activePreset.title }))
}
}, [apiRoute, activePreset?.id, activePreset?.title, t, presetConfig, i18n, resetQueryPreset])
const saveCurrentChanges = useCallback(async () => {
try {
await fetch(`${apiRoute}/payload-query-presets/${activePreset.id}`, {
body: JSON.stringify({
columns: transformColumnsToPreferences(query.columns),
where: query.where,
}),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'PATCH',
}).then(async (res) => {
try {
const json = await res.json()
if (res.status < 400) {
toast.success(
t('general:updatedLabelSuccessfully', {
label: getTranslation(presetConfig?.labels?.singular, i18n),
}),
)
setQueryModified(false)
} else {
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
toast.error(t('error:unknown'))
}
}
} catch (_err) {
toast.error(t('error:unknown'))
}
})
} catch (_err) {
toast.error(t('error:unknown'))
}
}, [
apiRoute,
activePreset?.id,
query.columns,
query.where,
t,
presetConfig?.labels?.singular,
i18n,
setQueryModified,
])
// Memoize so that components aren't re-rendered on query and column changes
const queryPresetMenuItems = useMemo(() => {
const menuItems: React.ReactNode[] = [
<PopupListGroupLabel
key="preset-group-label"
label={getTranslation(presetConfig?.labels?.plural, i18n)}
/>,
]
if (activePreset && modified) {
menuItems.push(
<PopupList.Button
onClick={async () => {
await refineListData(
{
columns: transformColumnsToSearchParams(activePreset.columns),
where: activePreset.where,
},
false,
)
}}
>
{t('general:reset')}
</PopupList.Button>,
)
if (queryPresetPermissions.update) {
menuItems.push(
<PopupList.Button
onClick={async () => {
await saveCurrentChanges()
}}
>
{activePreset.isShared ? t('general:updateForEveryone') : t('general:save')}
</PopupList.Button>,
)
}
}
menuItems.push(
<PopupList.Button
onClick={() => {
openCreateNewDrawer()
}}
>
{t('general:createNew')}
</PopupList.Button>,
)
if (activePreset && queryPresetPermissions?.delete) {
menuItems.push(
<Fragment>
<PopupList.Button onClick={() => openModal(confirmDeletePresetModalSlug)}>
{t('general:delete')}
</PopupList.Button>
<PopupList.Button
onClick={() => {
openDocumentDrawer()
}}
>
{t('general:edit')}
</PopupList.Button>
</Fragment>,
)
}
return menuItems
}, [
activePreset,
queryPresetPermissions?.delete,
queryPresetPermissions?.update,
openCreateNewDrawer,
openDocumentDrawer,
openModal,
saveCurrentChanges,
t,
refineListData,
modified,
presetConfig?.labels?.plural,
i18n,
])
return {
CreateNewPresetDrawer: (
<CreateNewPresetDrawer
initialData={{
columns: transformColumnsToPreferences(query.columns),
relatedCollection: collectionSlug,
where: query.where,
}}
onSave={async ({ doc }) => {
closeCreateNewDrawer()
await handlePresetChange(doc as QueryPreset)
}}
redirectAfterCreate={false}
/>
),
DeletePresetModal: (
<ConfirmationModal
body={
<Translation
elements={{
'1': ({ children }) => <strong>{children}</strong>,
}}
i18nKey="general:aboutToDelete"
t={t}
variables={{
label: presetConfig?.labels?.singular,
title: activePreset?.title,
}}
/>
}
confirmingLabel={t('general:deleting')}
heading={t('general:confirmDeletion')}
modalSlug={confirmDeletePresetModalSlug}
onConfirm={handleDeletePreset}
/>
),
EditPresetDrawer: (
<PresetDocumentDrawer
onDelete={() => {
// setSelectedPreset(undefined)
}}
onDuplicate={async ({ doc }) => {
await handlePresetChange(doc as QueryPreset)
}}
onSave={async ({ doc }) => {
await handlePresetChange(doc as QueryPreset)
}}
/>
),
hasModifiedPreset: modified,
openPresetListDrawer: openListDrawer,
PresetListDrawer: (
<ListDrawer
allowCreate={false}
disableQueryPresets
onSelect={async ({ doc }) => {
closeListDrawer()
await handlePresetChange(doc as QueryPreset)
}}
/>
),
queryPresetMenuItems,
resetPreset: resetQueryPreset,
}
}

View File

@@ -2,6 +2,7 @@
import type { ListQuery } from 'payload' import type { ListQuery } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { hoistQueryParamsToAnd } from 'payload/shared'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import type { ListDrawerProps } from './types.js' import type { ListDrawerProps } from './types.js'
@@ -10,7 +11,6 @@ import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js'
import { useEffectEvent } from '../../hooks/useEffectEvent.js' import { useEffectEvent } from '../../hooks/useEffectEvent.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { hoistQueryParamsToAnd } from '../../utilities/mergeListSearchAndWhere.js'
import { ListDrawerContextProvider } from '../ListDrawer/Provider.js' import { ListDrawerContextProvider } from '../ListDrawer/Provider.js'
import { LoadingOverlay } from '../Loading/index.js' import { LoadingOverlay } from '../Loading/index.js'
import { type Option } from '../ReactSelect/index.js' import { type Option } from '../ReactSelect/index.js'
@@ -18,6 +18,7 @@ import { type Option } from '../ReactSelect/index.js'
export const ListDrawerContent: React.FC<ListDrawerProps> = ({ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
allowCreate = true, allowCreate = true,
collectionSlugs, collectionSlugs,
disableQueryPresets,
drawerSlug, drawerSlug,
enableRowSelections, enableRowSelections,
filterOptions, filterOptions,
@@ -91,6 +92,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
collectionSlug: slug, collectionSlug: slug,
disableBulkDelete: true, disableBulkDelete: true,
disableBulkEdit: true, disableBulkEdit: true,
disableQueryPresets,
drawerSlug, drawerSlug,
enableRowSelections, enableRowSelections,
overrideEntityVisibility, overrideEntityVisibility,
@@ -117,6 +119,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
enableRowSelections, enableRowSelections,
filterOptions, filterOptions,
overrideEntityVisibility, overrideEntityVisibility,
disableQueryPresets,
], ],
) )

View File

@@ -7,6 +7,7 @@ import type { ListDrawerContextProps } from './Provider.js'
export type ListDrawerProps = { export type ListDrawerProps = {
readonly allowCreate?: boolean readonly allowCreate?: boolean
readonly collectionSlugs: SanitizedCollectionConfig['slug'][] readonly collectionSlugs: SanitizedCollectionConfig['slug'][]
readonly disableQueryPresets?: boolean
readonly drawerSlug?: string readonly drawerSlug?: string
readonly enableRowSelections?: boolean readonly enableRowSelections?: boolean
readonly filterOptions?: FilterOptionsResult readonly filterOptions?: FilterOptionsResult
@@ -28,10 +29,8 @@ export type UseListDrawer = (args: {
selectedCollection?: SanitizedCollectionConfig['slug'] selectedCollection?: SanitizedCollectionConfig['slug']
uploads?: boolean // finds all collections with upload: true uploads?: boolean // finds all collections with upload: true
}) => [ }) => [
React.FC< React.FC<Omit<ListDrawerProps, 'collectionSlugs'>>,
Pick<ListDrawerProps, 'allowCreate' | 'enableRowSelections' | 'onBulkSelect' | 'onSelect'> React.FC<Omit<ListTogglerProps, 'drawerSlug'>>,
>, // drawer
React.FC<Pick<ListTogglerProps, 'children' | 'className' | 'disabled' | 'onClick'>>, // toggler
{ {
closeDrawer: () => void closeDrawer: () => void
collectionSlugs: SanitizedCollectionConfig['slug'][] collectionSlugs: SanitizedCollectionConfig['slug'][]

View File

@@ -73,22 +73,28 @@
background: var(--theme-elevation-0); background: var(--theme-elevation-0);
&.pill--has-action { &.pill--has-action {
&:hover { &:hover,
background: var(--theme-elevation-100);
}
&:active { &:active {
background: var(--theme-elevation-100); background: var(--theme-elevation-100);
} }
} }
} }
&--style-always-white {
background: var(--theme-elevation-850);
color: var(--theme-elevation-0);
&.pill--has-action {
&:hover,
&:active {
background: var(--theme-elevation-750);
}
}
}
&--style-light { &--style-light {
&.pill--has-action { &.pill--has-action {
&:hover { &:hover,
background: var(--theme-elevation-100);
}
&:active { &:active {
background: var(--theme-elevation-100); background: var(--theme-elevation-100);
} }
@@ -139,4 +145,21 @@
line-height: 18px; line-height: 18px;
} }
} }
html[data-theme='light'] {
.pill {
&--style-always-white {
background: var(--theme-elevation-0);
color: var(--theme-elevation-800);
border: 1px solid var(--theme-elevation-100);
&.pill--has-action {
&:hover,
&:active {
background: var(--theme-elevation-100);
}
}
}
}
}
} }

View File

@@ -20,7 +20,15 @@ export type PillProps = {
icon?: React.ReactNode icon?: React.ReactNode
id?: string id?: string
onClick?: () => void onClick?: () => void
pillStyle?: 'dark' | 'error' | 'light' | 'light-gray' | 'success' | 'warning' | 'white' pillStyle?:
| 'always-white'
| 'dark'
| 'error'
| 'light'
| 'light-gray'
| 'success'
| 'warning'
| 'white'
rounded?: boolean rounded?: boolean
size?: 'medium' | 'small' size?: 'medium' | 'small'
to?: string to?: string
@@ -64,6 +72,7 @@ const DraggablePill: React.FC<PillProps> = (props) => {
const StaticPill: React.FC<PillProps> = (props) => { const StaticPill: React.FC<PillProps> = (props) => {
const { const {
id,
alignIcon = 'right', alignIcon = 'right',
'aria-checked': ariaChecked, 'aria-checked': ariaChecked,
'aria-controls': ariaControls, 'aria-controls': ariaControls,
@@ -101,6 +110,7 @@ const StaticPill: React.FC<PillProps> = (props) => {
if (onClick && !to) { if (onClick && !to) {
Element = 'button' Element = 'button'
} }
if (to) { if (to) {
Element = Link Element = Link
} }
@@ -114,6 +124,7 @@ const StaticPill: React.FC<PillProps> = (props) => {
aria-label={ariaLabel} aria-label={ariaLabel}
className={classes} className={classes}
href={to || null} href={to || null}
id={id}
onClick={onClick} onClick={onClick}
type={Element === 'button' ? 'button' : undefined} type={Element === 'button' ? 'button' : undefined}
> >

View File

@@ -8,6 +8,9 @@ import './index.scss'
const baseClass = 'popup-button-list' const baseClass = 'popup-button-list'
export { PopupListDivider as Divider } from '../PopupDivider/index.js'
export { PopupListGroupLabel as GroupLabel } from '../PopupGroupLabel/index.js'
export const ButtonGroup: React.FC<{ export const ButtonGroup: React.FC<{
buttonSize?: 'default' | 'small' buttonSize?: 'default' | 'small'
children: React.ReactNode children: React.ReactNode

View File

@@ -0,0 +1,10 @@
@import '../../../scss/styles.scss';
@layer payload-default {
.popup-divider {
width: 100%;
height: 1px;
background-color: var(--theme-elevation-150);
border: none;
}
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
import './index.scss'
const baseClass = 'popup-divider'
export const PopupListDivider: React.FC = () => {
return <hr className={baseClass} />
}

View File

@@ -0,0 +1,8 @@
@import '../../../scss/styles.scss';
@layer payload-default {
.popup-list-group-label {
color: var(--theme-elevation-500);
padding: 0 var(--list-button-padding);
}
}

View File

@@ -0,0 +1,11 @@
import React from 'react'
import './index.scss'
const baseClass = 'popup-list-group-label'
export const PopupListGroupLabel: React.FC<{
label: string
}> = ({ label }) => {
return <p className={baseClass}>{label}</p>
}

View File

@@ -7,8 +7,8 @@ import { useWindowInfo } from '@faceless-ui/window-info'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useIntersect } from '../../hooks/useIntersect.js' import { useIntersect } from '../../hooks/useIntersect.js'
import './index.scss'
import { PopupTrigger } from './PopupTrigger/index.js' import { PopupTrigger } from './PopupTrigger/index.js'
import './index.scss'
const baseClass = 'popup' const baseClass = 'popup'
@@ -60,6 +60,7 @@ export const Popup: React.FC<PopupProps> = (props) => {
verticalAlign: verticalAlignFromProps = 'top', verticalAlign: verticalAlignFromProps = 'top',
} = props } = props
const { height: windowHeight, width: windowWidth } = useWindowInfo() const { height: windowHeight, width: windowWidth } = useWindowInfo()
const [intersectionRef, intersectionEntry] = useIntersect({ const [intersectionRef, intersectionEntry] = useIntersect({
root: boundingRef?.current || null, root: boundingRef?.current || null,
rootMargin: '-100px 0px 0px 0px', rootMargin: '-100px 0px 0px 0px',
@@ -212,7 +213,7 @@ export const Popup: React.FC<PopupProps> = (props) => {
<div className={`${baseClass}__scroll-container`}> <div className={`${baseClass}__scroll-container`}>
<div className={`${baseClass}__scroll-content`}> <div className={`${baseClass}__scroll-content`}>
{render && render({ close: () => setActive(false) })} {render && render({ close: () => setActive(false) })}
{children && children} {children}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,30 @@
import type { DefaultCellComponentProps } from 'payload'
import type { JSX } from 'react'
import { toWords } from 'payload/shared'
import React, { Fragment } from 'react'
export const QueryPresetsAccessCell: React.FC<DefaultCellComponentProps> = ({ cellData }) => {
// first sort the operations in the order they should be displayed
const operations = ['read', 'update', 'delete']
return (
<p>
{operations.reduce((acc, operation, index) => {
const operationData = (cellData as JSON)?.[operation]
if (operationData && operationData.constraint) {
acc.push(
<Fragment key={operation}>
<span>
<strong>{toWords(operation)}</strong>: {toWords(operationData.constraint)}
</span>
{index !== operations.length - 1 && ', '}
</Fragment>,
)
}
return acc
}, [] as JSX.Element[])}
</p>
)
}

View File

@@ -0,0 +1,9 @@
@import '../../../../scss/styles';
@layer payload-default {
.query-preset-columns-cell {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
}

View File

@@ -0,0 +1,32 @@
import type { ColumnPreference, DefaultCellComponentProps } from 'payload'
import { toWords, transformColumnsToSearchParams } from 'payload/shared'
import React from 'react'
import { Pill } from '../../../Pill/index.js'
import './index.scss'
const baseClass = 'query-preset-columns-cell'
export const QueryPresetsColumnsCell: React.FC<DefaultCellComponentProps> = ({ cellData }) => {
return (
<div className={baseClass}>
{cellData
? transformColumnsToSearchParams(cellData as ColumnPreference[]).map((column, i) => {
const isColumnActive = !column.startsWith('-')
// to void very lengthy cells, only display the active columns
if (!isColumnActive) {
return null
}
return (
<Pill key={i} pillStyle={isColumnActive ? 'always-white' : 'light'}>
{toWords(column)}
</Pill>
)
})
: 'No columns selected'}
</div>
)
}

View File

@@ -0,0 +1,33 @@
import type { DefaultCellComponentProps, Where } from 'payload'
import { toWords } from 'payload/shared'
import React from 'react'
/** @todo: improve this */
const transformWhereToNaturalLanguage = (where: Where): string => {
if (where.or && where.or.length > 0 && where.or[0].and && where.or[0].and.length > 0) {
const orQuery = where.or[0]
const andQuery = orQuery?.and?.[0]
if (!andQuery) {
return 'No where query'
}
const key = Object.keys(andQuery)[0]
if (!andQuery[key]) {
return 'No where query'
}
const operator = Object.keys(andQuery[key])[0]
const value = andQuery[key][operator]
return `${toWords(key)} ${operator} ${toWords(value)}`
}
return ''
}
export const QueryPresetsWhereCell: React.FC<DefaultCellComponentProps> = ({ cellData }) => {
return <div>{cellData ? transformWhereToNaturalLanguage(cellData) : 'No where query'}</div>
}

View File

@@ -0,0 +1,17 @@
@import '../../../../scss/styles';
@layer payload-default {
.query-preset-columns-field {
.field-label {
margin-bottom: calc(var(--base) / 2);
}
.value-wrapper {
background-color: var(--theme-elevation-50);
padding: var(--base);
display: flex;
flex-wrap: wrap;
gap: calc(var(--base) / 2);
}
}
}

View File

@@ -0,0 +1,36 @@
'use client'
import type { ColumnPreference, JSONFieldClientComponent } from 'payload'
import { toWords, transformColumnsToSearchParams } from 'payload/shared'
import React from 'react'
import { FieldLabel } from '../../../../fields/FieldLabel/index.js'
import { useField } from '../../../../forms/useField/index.js'
import { Pill } from '../../../Pill/index.js'
import './index.scss'
export const QueryPresetsColumnField: JSONFieldClientComponent = ({
field: { label, required },
path,
}) => {
const { value } = useField({ path })
return (
<div className="field-type query-preset-columns-field">
<FieldLabel as="h3" label={label} path={path} required={required} />
<div className="value-wrapper">
{value
? transformColumnsToSearchParams(value as ColumnPreference[]).map((column, i) => {
const isColumnActive = !column.startsWith('-')
return (
<Pill key={i} pillStyle={isColumnActive ? 'always-white' : 'light-gray'}>
{toWords(column)}
</Pill>
)
})
: 'No columns selected'}
</div>
</div>
)
}

View File

@@ -0,0 +1,23 @@
@import '../../../../scss/styles';
@layer payload-default {
.query-preset-where-field {
.field-label {
margin-bottom: calc(var(--base) / 2);
}
.value-wrapper {
background-color: var(--theme-elevation-50);
padding: var(--base);
}
}
.query-preset-where-field {
.pill {
&--style-always-white {
background: var(--theme-elevation-250);
color: var(--theme-elevation-1000);
}
}
}
}

View File

@@ -0,0 +1,114 @@
'use client'
import type { JSONFieldClientComponent, Where } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { toWords } from 'payload/shared'
import React from 'react'
import { FieldLabel } from '../../../../fields/FieldLabel/index.js'
import { useField } from '../../../../forms/useField/index.js'
import { useConfig } from '../../../../providers/Config/index.js'
import { useListQuery } from '../../../../providers/ListQuery/index.js'
import { useTranslation } from '../../../../providers/Translation/index.js'
import { Pill } from '../../../Pill/index.js'
import './index.scss'
/** @todo: improve this */
const transformWhereToNaturalLanguage = (
where: Where,
collectionLabel: string,
): React.ReactNode => {
if (!where) {
return null
}
const renderCondition = (condition: any): React.ReactNode => {
const key = Object.keys(condition)[0]
if (!condition[key]) {
return 'No where query'
}
const operator = Object.keys(condition[key])[0]
let value = condition[key][operator]
// TODO: this is not right, but works for now at least.
// Ideally we look up iterate _fields_ so we know the type of the field
// Currently, we're only iterating over the `where` field's value, so we don't know the type
if (typeof value === 'object') {
try {
value = new Date(value).toLocaleDateString()
} catch (_err) {
value = 'Unknown error has occurred'
}
}
return (
<Pill pillStyle="always-white">
<b>{toWords(key)}</b> {operator} <b>{toWords(value)}</b>
</Pill>
)
}
const renderWhere = (where: Where, collectionLabel: string): React.ReactNode => {
if (where.or && where.or.length > 0) {
return (
<div className="or-condition">
{where.or.map((orCondition, orIndex) => (
<React.Fragment key={orIndex}>
{orCondition.and && orCondition.and.length > 0 ? (
<div className="and-condition">
{orIndex === 0 && (
<span className="label">{`Filter ${collectionLabel} where `}</span>
)}
{orIndex > 0 && <span className="label"> or </span>}
{orCondition.and.map((andCondition, andIndex) => (
<React.Fragment key={andIndex}>
{renderCondition(andCondition)}
{andIndex < orCondition.and.length - 1 && (
<span className="label"> and </span>
)}
</React.Fragment>
))}
</div>
) : (
renderCondition(orCondition)
)}
</React.Fragment>
))}
</div>
)
}
return renderCondition(where)
}
return renderWhere(where, collectionLabel)
}
export const QueryPresetsWhereField: JSONFieldClientComponent = ({
field: { label, required },
path,
}) => {
const { value } = useField({ path })
const { collectionSlug } = useListQuery()
const { getEntityConfig } = useConfig()
const collectionConfig = getEntityConfig({ collectionSlug })
const { i18n } = useTranslation()
return (
<div className="field-type query-preset-where-field">
<FieldLabel as="h3" label={label} path={path} required={required} />
<div className="value-wrapper">
{value
? transformWhereToNaturalLanguage(
value as Where,
getTranslation(collectionConfig.labels.plural, i18n),
)
: 'No where query'}
</div>
</div>
)
}

View File

@@ -5,9 +5,9 @@ import React, { useCallback } from 'react'
import type { DocumentDrawerProps } from '../../../DocumentDrawer/types.js' import type { DocumentDrawerProps } from '../../../DocumentDrawer/types.js'
import { EditIcon } from '../../../../icons/Edit/index.js' import { EditIcon } from '../../../../icons/Edit/index.js'
import { useCellProps } from '../../../../providers/TableColumns/RenderDefaultCell/index.js'
import { useDocumentDrawer } from '../../../DocumentDrawer/index.js' import { useDocumentDrawer } from '../../../DocumentDrawer/index.js'
import { DefaultCell } from '../../../Table/DefaultCell/index.js' import { DefaultCell } from '../../../Table/DefaultCell/index.js'
import { useCellProps } from '../../../TableColumns/RenderDefaultCell/index.js'
import './index.scss' import './index.scss'
export const DrawerLink: React.FC<{ export const DrawerLink: React.FC<{

Some files were not shown because too many files have changed in this diff Show More