Compare commits

...

44 Commits

Author SHA1 Message Date
Patrik Kozak
25fa48cd0f fix: resolve conflicts from PR 7355 2025-05-21 18:02:28 -04:00
Patrik Kozak
e8401d51a2 Merge branch 'main' of github.com:payloadcms/payload into fix/resolve-conflicts-from-7355 2025-05-21 17:12:16 -04:00
Anders Semb Hermansen
2a41d3fbb1 feat: show fields inside groups as separate columns in the list view (#7355)
## Description

Group fields are shown as one column, this PR changes this so that the
individual field is now shown separately.

Before change:
<img width="1227" alt="before change"
src="https://github.com/user-attachments/assets/dfae58fd-8ad2-4329-84fd-ed1d4eb20854">

After change:
<img width="1229" alt="after change"
src="https://github.com/user-attachments/assets/d4fd78bb-c474-436e-a0f5-cac4638b91a4">

- [X] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [X] New feature (non-breaking change which adds functionality)

## Checklist:

- [X] I have added tests that prove my fix is effective or that my
feature works
- [X] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
2025-05-21 16:25:34 -04:00
Jarrod Flesch
07e9444c09 fix build 2025-05-21 14:45:29 -04:00
Patrik
c772a3207c fix(ui): set gap to 0 in sort column buttons to remove unneeded spacing (#12481)
### What

This PR adjusts the `gap` between buttons in the `SortColumn` component.
The previous spacing (`calc(var(--base) / 4)`) caused too much visual
separation between the sort buttons. It has been replaced with `gap: 0`
to tighten their alignment.

#### Before:
![Screenshot 2025-05-21 at 1 33
17 PM](https://github.com/user-attachments/assets/a5f759fc-647a-46e3-8dac-e3e100fc7b98)

#### After:
![Screenshot 2025-05-21 at 1 34
04 PM](https://github.com/user-attachments/assets/29572620-bd62-4e3e-80b7-d32ed4c81911)
2025-05-21 11:33:14 -07:00
Germán Jabloñski
c701dd41a9 docs: update rich text to HTML conversion documentation (#12465)
Fixes #8168, #8277

The fact that `lexicalHTMLField` doesn't work with live preview was
already clarified at the beginning of the page. I mentioned it again in
the dedicated section because it seems there was still confusion.

Also, I reordered and hierarchized the headings correctly. The
introduction said there were two ways to convert to HTML, but there were
four headings with the same level. I also made the headings a little
shorter to make the table of contents easier to parse.
2025-05-21 15:24:31 -03:00
Jarrod Flesch
5dd13f2873 Merge branch 'feat/folders' into test/folders/e2e 2025-05-21 13:44:25 -04:00
Jarrod Flesch
878be913fd adjust test selectors 2025-05-21 13:43:28 -04:00
Paul
4dfb2d24bb feat(plugin-form-builder): add new date field (#12416)
Adds a new date field to take submission values for.

It can help form serialisers render the right input for this kind of
field as the submissions themselves don't do any validation right now.

Disabled by default as to not cause any conflicts with existing projects
potentially inserting their own date blocks.

Can be enabled like this

```ts
formBuilderPlugin({
   fields: {
     date: true
   }
})
```
2025-05-21 17:34:21 +00:00
Sasha
230128b92e fix(db-mongodb): remove limit from nested querying (#12464)
Fixes https://github.com/payloadcms/payload/issues/12456
2025-05-21 20:22:28 +03:00
Dan Ribbens
23f42040ab chore: ignore .idea run configuration templates (#12439)
Webstorm run configuration template files are trying to sneak into my
commits.
2025-05-21 13:13:55 -04:00
Alessio Gravili
8596ac5694 fix(richtext-lexical): support inline block types in strict mode for JSXConvertersFunction type (#12478)
Same as https://github.com/payloadcms/payload/pull/10398 but for inline
blocks.

> Reproduction steps:
> 1. Set `strict: true` in `templates/website/tsconfig.json`
> 2. You will find a ts error in
`templates/website/src/components/RichText/index.tsx`.
> 
> This is because the blockType property of blocks is generated by
Payload as a literal (e.g. "mediaBlock") and cannot be assigned to a
string.
> 
> To test this PR, you can make the change to `JSXConvertersFunction` in
node_modules of the website template
2025-05-21 16:54:03 +00:00
Jarrod Flesch
0524f198a6 require onCreateSuccess fn in ListCreateNewDocInFolderButton 2025-05-21 12:47:53 -04:00
Jarrod Flesch
5f2e846350 fixes cell not updating fromFolderID and missing onCreateSuccess fn 2025-05-21 12:47:00 -04:00
Jarrod Flesch
d3265b9931 add docs 2025-05-21 12:35:52 -04:00
Jessica Chowdhury
33261b36bf test: add e2e tests folder folder view - WIP 2025-05-21 17:28:01 +01:00
Jessica Chowdhury
73d4201df8 fix(ui): add prefix to folder drawer ids 2025-05-21 17:23:55 +01:00
Keisuke Ikeda
324daff553 docs: fix API capitalization typo in virtual fields documentation (#12477) 2025-05-21 15:56:58 +00:00
Jarrod Flesch
50d3da5824 remove enabled property from folders root config 2025-05-21 11:23:51 -04:00
Jarrod Flesch
41aac41df4 refactor how to enable a folder collection 2025-05-21 10:55:26 -04:00
Jarrod Flesch
6a5b95af7c fixes folderID with postgres 2025-05-21 09:12:49 -04:00
Jacob Fletcher
22b1858ee8 fix: auto inject req.user into query preset constraints (#12461)
In #12322 we prevented against accidental query preset lockout by
throwing a validation error when the user is going to change the preset
in a way that removes their own access to it. This, however, puts the
responsibility on the user to make the corrections and is an unnecessary
step.

For example, the API currently forbids leaving yourself out of the
`users` array when specifying the `specificUsers` constraint, but when
you encounter this error, have to update the field manually and try
again.

To improve the experience, we now automatically inject the requesting
user onto the `users` array when this constraint is selected. This will
guarantee they have access and prevent an accidental lockout while also
avoiding the API error feedback loop.
2025-05-20 17:15:18 -04:00
Jarrod Flesch
2e7bfcbd63 card grid sizing pt 3 2025-05-20 15:23:56 -04:00
Jarrod Flesch
3ee9a32a38 adjust card grid sizes 2025-05-20 15:22:48 -04:00
Jarrod Flesch
c2d38b4109 adjusts card width at larger screens 2025-05-20 15:20:52 -04:00
Jarrod Flesch
5d9c537145 update int tests 2025-05-20 15:08:53 -04:00
Jarrod Flesch
904b6a6dbe fix build 2025-05-20 14:14:52 -04:00
Jarrod Flesch
cc6de7ef42 rm console log 2025-05-20 14:10:40 -04:00
Jarrod Flesch
a3ef4fbfac fix filter bug 2025-05-20 14:10:08 -04:00
Jarrod Flesch
e9ff611879 more folder file name alignment 2025-05-20 14:09:43 -04:00
Jarrod Flesch
5825d0cfc7 align folder names with feature 2025-05-20 13:38:33 -04:00
Jarrod Flesch
103b476c82 Merge branch 'main' into feat/folders 2025-05-20 13:35:23 -04:00
Jarrod Flesch
a3279b319e update route structures to respect config routes 2025-05-20 13:29:58 -04:00
Jarrod Flesch
bbb0ab784c fix auotsave enabled collections 2025-05-20 13:04:17 -04:00
Jarrod Flesch
32eac5b0c2 rename test folder folder-view to folders 2025-05-20 12:28:14 -04:00
Jarrod Flesch
86098c9140 bug fixes 2025-05-20 10:55:45 -04:00
conico974
2ab8e2e194 fix: telemetry in opennext cloudflare (#12327)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
-->

### What?
This PR help to fix an issue you'll encounter while running payload in
OpenNext on cloudflare

### Why?
Sending telemetry event will create an infinite loop because it won't be
able to find a `package.json`

### How?
Putting the whole logic of `sendEvent` behind `config.telemetry` allows
to disable it and thus, make it work on cloudflare

See this comment for more info :
https://github.com/opennextjs/opennextjs-cloudflare/issues/263#issuecomment-2851747956
2025-05-20 07:49:23 -07:00
Patrik
1235a183ff fix: prevent resizing of original file with withoutEnlargement on update (#12291)
This PR updates `generateFileData` to skip applying `resizeOptions`
after updating an image if `resizeOptions.withoutEnlargement` is `true`
and the original image size is smaller than the dimensions defined in
`resizeOptions`.

This prevents unintended re-resizing of already resized images when
updating or modifying metadata without uploading a new file.

This change ensures that:

- Resizing is skipped if withoutEnlargement: true

- Resizing still occurs if withoutEnlargement: false or unset

This resolves an issue where images were being resized again
unnecessarily when updating an upload.

Fixes #12280
2025-05-20 06:43:53 -07:00
Sasha
81d333f4b0 test: add test for sorting by a virtual field with a reference (#12351) 2025-05-20 13:07:48 +00:00
Sasha
4fe3423e54 fix(plugin-multi-tenant): multi-locale tenant select label (#12444)
fixes https://github.com/payloadcms/payload/issues/12443
2025-05-20 05:02:47 -07:00
Jarrod Flesch
7fd2cdf04c allow for folders name to be configurable 2025-05-19 21:00:44 -04:00
Paul
e8c2b15e2b fix(plugin-multi-tenant): add missing translation for Assigned Tenant field (#12448)
Previously the "Assigned Tenant" field didn't have a translated label
2025-05-19 13:20:12 -07:00
Paul
3127d6ad6d fix(plugin-import-export): add translations for all UI elements and fields (#12449)
Converts all text and field labels into variables that can be
translated. Also generated the translations for them

So now the UI here is internationalised


![image](https://github.com/user-attachments/assets/40d7c010-ac58-4cd7-8786-01b3de3cabb7)

I've also moved some of the generic labels into the core package since
those could be re-used elsewhere
2025-05-19 13:19:55 -07:00
Paul
72ab319d37 fix(db-*): ensure consistent sorting even when sorting on non-unique fields or no sort parameters at all (#12447)
The databases do not keep track of document order internally so when
sorting by non-unique fields such as shared `order` number values, the
returned order will be random and not consistent.

While this issue is far more noticeable on mongo it could also occur in
postgres on certain environments.

This combined with pagination can lead to the perception of duplicated
or inconsistent data.

This PR adds a second sort parameter to queries so that we always have a
fallback, `-createdAt` will be used by default or `id` if timestamps are
disabled.
2025-05-19 12:59:12 -07:00
262 changed files with 4729 additions and 876 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ package-lock.json
dist
/.idea/*
!/.idea/runConfigurations
/.idea/runConfigurations/_template*
!/.idea/payload.iml
# Custom actions

View File

@@ -132,6 +132,7 @@ The following options are available:
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |

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). |
| **`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. |
| `folders` | An optional object to configure global folder settings. [More details](../folders/overview). |
| `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). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |

View File

@@ -100,7 +100,7 @@ Here are the available Presentational Fields:
### Virtual Fields
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the APi response through hooks, etc.
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the API response through hooks, etc.
Here are the available Virtual Fields:

100
docs/folders/overview.mdx Normal file
View File

@@ -0,0 +1,100 @@
---
title: Folders
label: Folders
order: 10
desc: Folders allow you to group documents across collections, and are a great way to organize your content.
keywords: folders, folder, content organization
---
Folders allow you to group documents across collections, and are a great way to organize your content. Folders are built on top of relationship fields, when you enable folders on a collection, Payload adds a hidden relationship field `folders`, that relates to a folder — or no folder. Folders also have the `folder` field, allowing folders to be nested within other folders.
The configuration for folders is done in two places, the collection config and the Payload config. The collection config is where you enable folders, and the Payload config is where you configure the global folder settings.
## Folder Configuration
On the payload config, you can configure the following settings under the `folders` property:
```ts
// Type definition
type RootFoldersConfiguration = {
/**
* An array of functions to be ran when the folder collection is initialized
* This allows plugins to modify the collection configuration
*/
collectionOverrides?: (({
collection,
}: {
collection: CollectionConfig
}) => CollectionConfig | Promise<CollectionConfig>)[]
/**
* Ability to view hidden fields and collections related to folders
*
* @default false
*/
debug?: boolean
/**
* The Folder field name
*
* @default "folder"
*/
fieldName?: string
/**
* Slug for the folder collection
*
* @default "payload-folders"
*/
slug?: string
}
```
```ts
// Example usage
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
folders: {
// highlight-start
debug: true, // optional
collectionOverrides: [
async ({ collection }) => {
return collection
},
], // optional
fieldName: 'folder', // optional
slug: 'payload-folders', // optional
// highlight-end
},
})
```
## Collection Configuration
To enable folders on a collection, you need to set the `admin.folders` property to `true` on the collection config. This will add a hidden relationship field to the collection that relates to a folder — or no folder.
```ts
// Type definition
type CollectionFoldersConfiguration = boolean
```
```ts
// Example usage
import { buildConfig } from 'payload'
const config = buildConfig({
collections: [
{
slug: 'pages',
// highlight-start
admin: {
folders: true, // defaults to false
},
// highlight-end
},
],
})
```

View File

@@ -85,6 +85,7 @@ formBuilderPlugin({
checkbox: true,
number: true,
message: true,
date: false,
payment: false,
},
})
@@ -349,6 +350,18 @@ Maps to a `checkbox` input on your front-end. Used to collect a boolean value.
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
### Date
Maps to a `date` input on your front-end. Used to collect a date value.
| Property | Type | Description |
| -------------- | -------- | ---------------------------------------------------- |
| `name` | string | The name of the field. |
| `label` | string | The label of the field. |
| `defaultValue` | date | The default value of the field. |
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
### Number
Maps to a `number` input on your front-end. Used to collect a number.
@@ -421,6 +434,42 @@ formBuilderPlugin({
})
```
### Customizing the date field default value
You can custommise the default value of the date field and any other aspects of the date block in this way.
Note that the end submission source will be responsible for the timezone of the date. Payload only stores the date in UTC format.
```ts
import { fields as formFields } from '@payloadcms/plugin-form-builder'
// payload.config.ts
formBuilderPlugin({
fields: {
// date: true, // just enable it without any customizations
date: {
...formFields.date,
fields: [
...(formFields.date && 'fields' in formFields.date
? formFields.date.fields.map((field) => {
if ('name' in field && field.name === 'defaultValue') {
return {
...field,
timezone: true, // optionally enable timezone
admin: {
...field.admin,
description: 'This is a date field',
},
}
}
return field
})
: []),
],
},
},
})
```
## Email
This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided.

View File

@@ -6,14 +6,14 @@ desc: Converting between lexical richtext and HTML
keywords: lexical, richtext, html
---
## Converting Rich Text to HTML
## Rich Text to HTML
There are two main approaches to convert your Lexical-based rich text to HTML:
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
### Generating HTML on-demand (Recommended)
### On-demand
To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend:
@@ -32,61 +32,81 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
}
```
### Converting Lexical Blocks
#### Dynamic Population (Advanced)
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
})
const [html, setHTML] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({
apiURL: `http://localhost:3000/api`,
}),
})
setHTML(html)
}
return <div dangerouslySetInnerHTML={{ __html: html }} />
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Outputting HTML from the Collection
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
To automatically generate HTML from the saved richText field in your Collection, use the `lexicalHTMLField()` helper. This approach converts the JSON to HTML using an `afterRead` hook. For instance:
```tsx
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import React from 'react'
import config from '../../config.js'
export const MyRSCComponent = async ({
data,
}: {
data: SerializedEditorState
}) => {
const payload = await getPayload({
config,
})
const html = await convertLexicalToHTMLAsync({
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### HTML field
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended for two reasons:
1. It creates a column with duplicate content in another format.
2. In [client-side live preview](/docs/live-preview/client), it makes it not "live".
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
@@ -154,74 +174,59 @@ const Pages: CollectionConfig = {
}
```
### Generating HTML in Your Frontend with Dynamic Population (Advanced)
## Blocks to HTML
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const [html, setHTML] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({
apiURL: `http://localhost:3000/api`,
}),
})
setHTML(html)
}
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
```tsx
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
import config from '../../config.js'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
export const MyRSCComponent = async ({
data,
}: {
data: SerializedEditorState
}) => {
const payload = await getPayload({
config,
})
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
const html = await convertLexicalToHTMLAsync({
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Converting HTML to Richtext
## HTML to Richtext
If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](/docs/rich-text/converters#retrieving-the-editor-config):

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and JSX
keywords: lexical, richtext, jsx
---
## Converting Richtext to JSX
## Richtext to JSX
To convert richtext to JSX, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the richtext content to it:
@@ -28,7 +28,7 @@ The `RichText` component includes built-in converters for common Lexical nodes.
populated data to work correctly.
</Banner>
### Converting Internal Links
### Internal Links
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
@@ -81,7 +81,7 @@ export const MyComponent: React.FC<{
}
```
### Converting Lexical Blocks
### Lexical Blocks
If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
@@ -133,7 +133,7 @@ export const MyComponent: React.FC<{
}
```
### Overriding Default JSX Converters
### Overriding Converters
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and Markdown / MDX
keywords: lexical, richtext, markdown, md, mdx
---
## Converting Richtext to Markdown
## Richtext to Markdown
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert the lexical editor state to Markdown with the following:
@@ -91,7 +91,7 @@ const Pages: CollectionConfig = {
}
```
## Converting Markdown to Richtext
## Markdown to Richtext
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert Markdown to the lexical editor state with the following:

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and plaintext
keywords: lexical, richtext, plaintext, text
---
## Converting Richtext to Plaintext
## Richtext to Plaintext
Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`.

View File

@@ -20,7 +20,6 @@ type SearchParam = {
const subQueryOptions = {
lean: true,
limit: 50,
}
/**
@@ -184,7 +183,7 @@ export async function buildSearchParam({
select[joinPath] = true
}
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
const result = await SubModel.find(subQuery).lean().select(select)
const $in: unknown[] = []

View File

@@ -150,6 +150,18 @@ export const buildSortParam = ({
sort = [sort]
}
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
// So we add a fallback sort to ensure that the results are always in the same order.
let fallbackSort = '-id'
if (timestamps) {
fallbackSort = '-createdAt'
}
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
sort.push(fallbackSort)
}
const sorting = sort.reduce<Record<string, string>>((acc, item) => {
let sortProperty: string
let sortDirection: SortDirection

View File

@@ -39,8 +39,9 @@ export const buildOrderBy = ({
}: Args): BuildQueryResult['orderBy'] => {
const orderBy: BuildQueryResult['orderBy'] = []
const createdAt = adapter.tables[tableName]?.createdAt
if (!sort) {
const createdAt = adapter.tables[tableName]?.createdAt
if (createdAt) {
sort = '-createdAt'
} else {
@@ -52,6 +53,18 @@ export const buildOrderBy = ({
sort = [sort]
}
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
// So we add a fallback sort to ensure that the results are always in the same order.
let fallbackSort = '-id'
if (createdAt) {
fallbackSort = '-createdAt'
}
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
sort.push(fallbackSort)
}
for (const sortItem of sort) {
let sortProperty: string
let sortDirection: 'asc' | 'desc'

View File

@@ -21,13 +21,22 @@ export const DefaultNavClient: React.FC<{
const {
config: {
admin: {
routes: { folders: foldersRoute },
routes: { browseByFolder: foldersRoute },
},
folders: { collections: folderCollections = {}, enabled } = {},
collections,
routes: { admin: adminRoute },
},
} = useConfig()
const [folderCollectionSlugs] = React.useState<string[]>(() => {
return collections.reduce<string[]>((acc, collection) => {
if (collection.admin.folders) {
acc.push(collection.slug)
}
return acc
}, [])
})
const { i18n } = useTranslation()
const folderURL = formatAdminURL({
@@ -39,9 +48,7 @@ export const DefaultNavClient: React.FC<{
return (
<Fragment>
{enabled && Object.keys(folderCollections).length > 0 && (
<BrowseByFolderButton active={viewingRootFolderView} />
)}
{folderCollectionSlugs.length > 0 && <BrowseByFolderButton active={viewingRootFolderView} />}
{groups.map(({ entities, label }, key) => {
return (
<NavGroup isOpen={navPreferences?.groups?.[label]?.open} key={key} label={label}>

View File

@@ -5,13 +5,15 @@ import type {
ListQuery,
} from 'payload'
import { DefaultFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { redirect } from 'next/navigation.js'
import { getFolderData } from 'payload'
import React from 'react'
import { getPreferences } from '../../utilities/getPreferences.js'
export type BuildFolderViewArgs = {
customCellProps?: Record<string, any>
disableBulkDelete?: boolean
@@ -23,13 +25,14 @@ export type BuildFolderViewArgs = {
query: ListQuery
} & AdminViewServerProps
export const buildFolderView = async (
export const buildBrowseByFolderView = async (
args: BuildFolderViewArgs,
): Promise<BuildCollectionFolderViewResult> => {
const {
disableBulkDelete,
disableBulkEdit,
enableRowSelections,
folderCollectionSlugs,
folderID,
initPageResult,
isInDrawer,
@@ -51,9 +54,7 @@ export const buildFolderView = async (
visibleEntities,
} = initPageResult
const allFolderCollectionSlugs = Object.keys(config?.folders?.collections || {})
const collections = allFolderCollectionSlugs.filter(
const collections = folderCollectionSlugs.filter(
(collectionSlug) =>
permissions?.collections?.[collectionSlug]?.read &&
visibleEntities.collections.includes(collectionSlug),
@@ -64,11 +65,10 @@ export const buildFolderView = async (
}
const query = queryFromArgs || queryFromReq
// get relationTo filter from query params
const selectedCollectionSlugs: string[] =
Array.isArray(query?.relationTo) && query.relationTo.length
? query.relationTo
: [...allFolderCollectionSlugs, config.folders.slug]
: [...folderCollectionSlugs, config.folders.slug]
const {
routes: { admin: adminRoute },
@@ -85,18 +85,25 @@ export const buildFolderView = async (
if (
!isInDrawer &&
((resolvedFolderID && folderID && folderID !== String(resolvedFolderID)) ||
((resolvedFolderID && folderID && folderID !== resolvedFolderID) ||
(folderID && !resolvedFolderID))
) {
return redirect(
formatAdminURL({
adminRoute,
path: config.admin.routes.folders,
path: config.admin.routes.browseByFolder,
serverURL: config.serverURL,
}),
)
}
const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>(
'browse-by-folder',
payload,
user.id,
user.collection,
)
const serverProps: Omit<FolderListViewServerPropsOnly, 'collectionConfig' | 'listPreferences'> = {
documents,
i18n,
@@ -119,7 +126,7 @@ export const buildFolderView = async (
// documents cannot be created without a parent folder in this view
const hasCreatePermissionCollectionSlugs = folderID
? [config.folders.slug, ...allFolderCollectionSlugs]
? [config.folders.slug, ...folderCollectionSlugs]
: [config.folders.slug]
return {
@@ -128,6 +135,7 @@ export const buildFolderView = async (
breadcrumbs={breadcrumbs}
documents={documents}
filteredCollectionSlugs={selectedCollectionSlugs}
folderCollectionSlugs={folderCollectionSlugs}
folderID={folderID}
subfolders={subfolders}
>
@@ -140,9 +148,10 @@ export const buildFolderView = async (
enableRowSelections,
hasCreatePermissionCollectionSlugs,
selectedCollectionSlugs,
viewPreference: browseByFolderPreferences?.value?.viewPreference,
},
// Component:config.folders?.components?.views?.list?.Component,
Fallback: DefaultFolderView,
Fallback: DefaultBrowseByFolderView,
importMap: payload.importMap,
serverProps,
})}

View File

@@ -4,11 +4,11 @@ import { notFound } from 'next/navigation.js'
import type { BuildFolderViewArgs } from './buildView.js'
import { buildFolderView } from './buildView.js'
import { buildBrowseByFolderView } from './buildView.js'
export const FolderView: React.FC<BuildFolderViewArgs> = async (args) => {
export const BrowseByFolder: React.FC<BuildFolderViewArgs> = async (args) => {
try {
const { View } = await buildFolderView(args)
const { View } = await buildBrowseByFolderView(args)
return View
} catch (error) {
if (error.message === 'not-found') {

View File

@@ -13,6 +13,8 @@ import { redirect } from 'next/navigation.js'
import { getFolderData, parseDocumentID } from 'payload'
import React from 'react'
import { getPreferences } from '../../utilities/getPreferences.js'
// import { renderFolderViewSlots } from './renderFolderViewSlots.js'
export type BuildCollectionFolderViewStateArgs = {
@@ -35,6 +37,7 @@ export const buildCollectionFolderView = async (
disableBulkDelete,
disableBulkEdit,
enableRowSelections,
folderCollectionSlugs,
folderID,
initPageResult,
isInDrawer,
@@ -66,12 +69,12 @@ export const buildCollectionFolderView = async (
if (collectionConfig) {
const query = queryFromArgs || queryFromReq
// const collectionFolderPreferences = await upsertPreferences<ListPreferences>({
// key: `${collectionSlug}-collection-folder`,
// req,
// value: {
// },
// })
const collectionFolderPreferences = await getPreferences<{ viewPreference: string }>(
`${collectionSlug}-collection-folder`,
payload,
user.id,
user.collection,
)
const {
routes: { admin: adminRoute },
@@ -79,7 +82,7 @@ export const buildCollectionFolderView = async (
if (
(!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) ||
!Object.keys(config.folders.collections).includes(collectionSlug)
!folderCollectionSlugs.includes(collectionSlug)
) {
throw new Error('not-found')
}
@@ -94,13 +97,13 @@ export const buildCollectionFolderView = async (
if (folderID) {
whereConstraints.push({
_folder: {
[config.folders.fieldName]: {
equals: parseDocumentID({ id: folderID, collectionSlug, payload }),
},
})
} else {
whereConstraints.push({
_folder: {
[config.folders.fieldName]: {
exists: false,
},
})
@@ -118,13 +121,13 @@ export const buildCollectionFolderView = async (
if (
!isInDrawer &&
((resolvedFolderID && folderID && folderID !== String(resolvedFolderID)) ||
((resolvedFolderID && folderID && folderID !== resolvedFolderID) ||
(folderID && !resolvedFolderID))
) {
return redirect(
formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/folders`,
path: `/collections/${collectionSlug}/${config.folders.slug}`,
serverURL: config.serverURL,
}),
)
@@ -173,6 +176,7 @@ export const buildCollectionFolderView = async (
breadcrumbs={breadcrumbs}
collectionSlug={collectionSlug}
documents={documents}
folderCollectionSlugs={folderCollectionSlugs}
folderID={folderID}
search={search}
subfolders={subfolders}
@@ -187,6 +191,7 @@ export const buildCollectionFolderView = async (
enableRowSelections,
hasCreatePermission,
newDocumentURL,
viewPreference: collectionFolderPreferences?.value?.viewPreference,
},
Component: collectionConfig?.admin?.components?.views?.list?.Component,
Fallback: DefaultCollectionFolderView,

View File

@@ -1,5 +1,6 @@
import type {
AdminViewServerProps,
CollectionSlug,
DocumentSubViewTypes,
ImportMap,
PayloadComponent,
@@ -14,11 +15,11 @@ import { formatAdminURL } from 'payload/shared'
import type { initPage } from '../../utilities/initPage/index.js'
import { Account } from '../Account/index.js'
import { BrowseByFolder } from '../BrowseByFolder/index.js'
import { CollectionFolderView } from '../CollectionFolders/index.js'
import { CreateFirstUserView } from '../CreateFirstUser/index.js'
import { Dashboard } from '../Dashboard/index.js'
import { Document as DocumentView } from '../Document/index.js'
import { FolderView } from '../Folders/index.js'
import { forgotPasswordBaseClass, ForgotPasswordView } from '../ForgotPassword/index.js'
import { ListView } from '../List/index.js'
import { loginBaseClass, LoginView } from '../Login/index.js'
@@ -51,8 +52,8 @@ export type ViewFromConfig = {
const oneSegmentViews: OneSegmentViews = {
account: Account,
browseByFolder: BrowseByFolder,
createFirstUser: CreateFirstUserView,
folders: FolderView,
forgot: ForgotPasswordView,
inactivity: LogoutInactivity,
login: LoginView,
@@ -74,6 +75,7 @@ type GetRouteDataArgs = {
type GetRouteDataResult = {
DefaultView: ViewFromConfig
documentSubViewType?: DocumentSubViewTypes
folderCollectionSlugs: CollectionSlug[]
folderID?: string
initPageOptions: Parameters<typeof initPage>[0]
serverProps: ServerPropsFromView
@@ -110,7 +112,13 @@ export const getRouteData = ({
const isCollection = segmentOne === 'collections'
let matchedCollection: SanitizedConfig['collections'][number] = undefined
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
const isFolderViewEnabled = config.folders?.enabled
const folderCollectionSlugs = config.collections.reduce((acc, { slug, admin }) => {
if (admin?.folders) {
return [...acc, slug]
}
return acc
}, [])
const serverProps: ServerPropsFromView = {
viewActions: config?.admin?.components?.actions || [],
@@ -160,7 +168,7 @@ export const getRouteData = ({
if (oneSegmentViews[viewKey]) {
// --> /account
// --> /create-first-user
// --> /folders
// --> /browse-by-folder
// --> /forgot
// --> /login
// --> /logout
@@ -174,15 +182,15 @@ export const getRouteData = ({
templateClassName = baseClasses[viewKey]
templateType = 'minimal'
if (isFolderViewEnabled && viewKey === 'folders') {
templateType = 'default'
viewType = 'folders'
}
if (viewKey === 'account') {
templateType = 'default'
viewType = 'account'
}
if (folderCollectionSlugs.length && viewKey === 'browseByFolder') {
templateType = 'default'
viewType = 'folders'
}
}
break
}
@@ -195,17 +203,18 @@ export const getRouteData = ({
templateClassName = baseClasses[segmentTwo]
templateType = 'minimal'
viewType = 'reset'
} else if (`/${segmentOne}` === config.admin.routes.folders) {
if (isFolderViewEnabled) {
// --> /folders/:folderID
ViewToRender = {
Component: oneSegmentViews.folders,
}
templateClassName = baseClasses.folders
templateType = 'default'
viewType = 'folders'
folderID = segmentTwo
} else if (
folderCollectionSlugs.length &&
`/${segmentOne}` === config.admin.routes.browseByFolder
) {
// --> /browse-by-folder/:folderID
ViewToRender = {
Component: oneSegmentViews.browseByFolder,
}
templateClassName = baseClasses.folders
templateType = 'default'
viewType = 'folders'
folderID = segmentTwo
} else if (isCollection && matchedCollection) {
// --> /collections/:collectionSlug
@@ -251,23 +260,21 @@ export const getRouteData = ({
templateType = 'minimal'
viewType = 'verify'
} else if (isCollection && matchedCollection) {
if (segmentThree === 'folders') {
if (
isFolderViewEnabled &&
Object.keys(config.folders.collections).includes(matchedCollection.slug)
) {
// Collection Folder Views
// --> /collections/:collectionSlug/folders
// --> /collections/:collectionSlug/folders/:folderID
ViewToRender = {
Component: CollectionFolderView,
}
templateClassName = `collection-folders`
templateType = 'default'
viewType = 'collection-folders'
folderID = segmentFour
if (
segmentThree === config.folders.slug &&
folderCollectionSlugs.includes(matchedCollection.slug)
) {
// Collection Folder Views
// --> /collections/:collectionSlug/:folderCollectionSlug
// --> /collections/:collectionSlug/:folderCollectionSlug/:folderID
ViewToRender = {
Component: CollectionFolderView,
}
templateClassName = `collection-folders`
templateType = 'default'
viewType = 'collection-folders'
folderID = segmentFour
} else {
// Collection Edit Views
// --> /collections/:collectionSlug/:id
@@ -328,6 +335,7 @@ export const getRouteData = ({
return {
DefaultView: ViewToRender,
documentSubViewType,
folderCollectionSlugs,
folderID,
initPageOptions,
serverProps,

View File

@@ -1,15 +1,16 @@
import type { I18nClient } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type {
AdminViewClientProps,
AdminViewServerPropsOnly,
ImportMap,
SanitizedConfig,
} from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { notFound, redirect } from 'next/navigation.js'
import {
type AdminViewClientProps,
type AdminViewServerPropsOnly,
type ImportMap,
parseDocumentID,
type SanitizedConfig,
} from 'payload'
import { formatAdminURL } from 'payload/shared'
import React from 'react'
@@ -64,7 +65,8 @@ export const RootPage = async ({
const {
DefaultView,
documentSubViewType,
folderID,
folderCollectionSlugs,
folderID: folderIDParam,
initPageOptions,
serverProps,
templateClassName,
@@ -137,8 +139,20 @@ export const RootPage = async ({
importMap,
})
const payload = initPageResult?.req.payload
const folderID = parseDocumentID({
id: folderIDParam,
collectionSlug: payload.config.folders.slug,
payload,
})
const RenderedView = RenderServerComponent({
clientProps: { clientConfig, documentSubViewType, viewType } satisfies AdminViewClientProps,
clientProps: {
clientConfig,
documentSubViewType,
folderCollectionSlugs,
viewType,
} satisfies AdminViewClientProps,
Component: DefaultView.payloadComponent,
Fallback: DefaultView.Component,
importMap,

View File

@@ -3,11 +3,11 @@ import type { SanitizedConfig } from 'payload'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { generateAccountViewMetadata } from '../Account/metadata.js'
import { generateBrowseByFolderMetadata } from '../BrowseByFolder/metadata.js'
import { generateCollectionFolderMetadata } from '../CollectionFolders/metadata.js'
import { generateCreateFirstUserViewMetadata } from '../CreateFirstUser/metadata.js'
import { generateDashboardViewMetadata } from '../Dashboard/metadata.js'
import { generateDocumentViewMetadata } from '../Document/metadata.js'
import { generateBrowseByFolderMetadata } from '../Folders/metadata.js'
import { generateForgotPasswordViewMetadata } from '../ForgotPassword/metadata.js'
import { generateListViewMetadata } from '../List/metadata.js'
import { generateLoginViewMetadata } from '../Login/metadata.js'
@@ -43,8 +43,14 @@ export const generatePageMetadata = async ({
params: paramsPromise,
}: Args) => {
const config = await configPromise
const params = await paramsPromise
const folderCollectionSlugs = config.collections.reduce((acc, { slug, admin }) => {
if (admin?.folders) {
return [...acc, slug]
}
return acc
}, [])
const segments = Array.isArray(params.segments) ? params.segments : []
const currentRoute = `/${segments.join('/')}`
@@ -75,20 +81,22 @@ export const generatePageMetadata = async ({
break
}
case 1: {
if (oneSegmentMeta[segmentOne] && segmentOne !== 'account') {
if (folderCollectionSlugs.length && `/${segmentOne}` === config.admin.routes.browseByFolder) {
// --> /:folderCollectionSlug
meta = await oneSegmentMeta.folders({ config, i18n })
} else if (segmentOne === 'account') {
// --> /account
meta = await generateAccountViewMetadata({ config, i18n })
break
} else if (oneSegmentMeta[segmentOne]) {
// --> /create-first-user
// --> /forgot
// --> /folders
// --> /login
// --> /logout
// --> /logout-inactivity
// --> /unauthorized
meta = await oneSegmentMeta[segmentOne]({ config, i18n })
break
} else if (segmentOne === 'account') {
// --> /account
meta = await generateAccountViewMetadata({ config, i18n })
break
}
break
}
@@ -96,8 +104,11 @@ export const generatePageMetadata = async ({
if (`/${segmentOne}` === config.admin.routes.reset) {
// --> /reset/:token
meta = await generateResetPasswordViewMetadata({ config, i18n })
} else if (`/${segmentOne}` === config.admin.routes.folders) {
// --> /folders/:folderID
} else if (
folderCollectionSlugs.length &&
`/${segmentOne}` === config.admin.routes.browseByFolder
) {
// --> /browse-by-folder/:folderID
meta = await generateBrowseByFolderMetadata({ config, i18n })
} else if (isCollection) {
// --> /collections/:collectionSlug
@@ -118,11 +129,11 @@ export const generatePageMetadata = async ({
// --> /:collectionSlug/verify/:token
meta = await generateVerifyViewMetadata({ config, i18n })
} else if (isCollection) {
if (segmentThree === 'folders') {
if (Object.keys(config.folders.collections).includes(collectionConfig.slug)) {
if (segmentThree === config.folders.slug) {
if (folderCollectionSlugs.includes(collectionConfig.slug)) {
// Collection Folder Views
// --> /collections/:collectionSlug/folders
// --> /collections/:collectionSlug/folders/:id
// --> /collections/:collectionSlug/:folderCollectionSlug
// --> /collections/:collectionSlug/:folderCollectionSlug/:id
meta = await generateCollectionFolderMetadata({
collectionConfig,
config,

View File

@@ -1,6 +1,6 @@
import type { ArrayFieldClient, BlocksFieldClient, ClientConfig, ClientField } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import { fieldShouldBeLocalized, groupHasName } from 'payload/shared'
import { fieldHasChanges } from './fieldHasChanges.js'
import { getFieldsForRowComparison } from './getFieldsForRowComparison.js'
@@ -114,25 +114,37 @@ export function countChangedFields({
// Fields that have nested fields and nest their fields' data.
case 'group': {
if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) {
locales.forEach((locale) => {
if (groupHasName(field)) {
if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) {
locales.forEach((locale) => {
count += countChangedFields({
comparison: comparison?.[field.name]?.[locale],
config,
fields: field.fields,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
version: version?.[field.name]?.[locale],
})
})
} else {
count += countChangedFields({
comparison: comparison?.[field.name]?.[locale],
comparison: comparison?.[field.name],
config,
fields: field.fields,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
version: version?.[field.name]?.[locale],
version: version?.[field.name],
})
})
}
} else {
// Unnamed group field: data is NOT nested under `field.name`
count += countChangedFields({
comparison: comparison?.[field.name],
comparison,
config,
fields: field.fields,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
version: version?.[field.name],
version,
})
}
break

View File

@@ -27,6 +27,7 @@ export type FolderListViewClientProps = {
enableRowSelections?: boolean
hasCreatePermission: boolean
newDocumentURL: string
viewPreference: 'grid' | 'list'
} & FolderListViewSlots
export type FolderListViewSlotSharedClientProps = {

View File

@@ -31,6 +31,7 @@ export type AdminViewConfig = {
export type AdminViewClientProps = {
clientConfig: ClientConfig
documentSubViewType?: DocumentSubViewTypes
folderCollectionSlugs?: SanitizedCollectionConfig['slug'][]
viewType: ViewTypes
}
@@ -41,7 +42,7 @@ export type AdminViewServerPropsOnly = {
* @todo remove `docID` here as it is already contained in `initPageResult`
*/
readonly docID?: number | string
readonly folderID?: string
readonly folderID?: number | string
readonly importMap: ImportMap
readonly initialData?: Data
readonly initPageResult: InitPageResult

View File

@@ -69,6 +69,7 @@ export const addDefaultsToCollectionConfig = (collection: CollectionConfig): Col
custom: {},
enableRichTextLink: true,
enableRichTextRelationship: true,
folders: false,
useAsTitle: 'id',
...(collection.admin || {}),
pagination: {

View File

@@ -32,6 +32,7 @@ import type {
RelationshipField,
UploadField,
} from '../../fields/config/types.js'
import type { CollectionFoldersConfiguration } from '../../folders/types.js'
import type {
CollectionSlug,
JsonObject,
@@ -344,6 +345,10 @@ export type CollectionAdminOptions = {
disableCopyToLocale?: boolean
enableRichTextLink?: boolean
enableRichTextRelationship?: boolean
/**
* Enables folders for this collection
*/
folders?: CollectionFoldersConfiguration
/**
* Specify a navigational group for collections in the admin sidebar.
* - Provide a string to place the entity in a custom group.

View File

@@ -2,6 +2,7 @@ import type { JobsConfig } from '../queues/config/types/index.js'
import type { Config } from './types.js'
import defaultAccess from '../auth/defaultAccess.js'
import { foldersSlug, parentFolderFieldName } from '../folders/constants.js'
/**
* @deprecated - remove in 4.0. This is error-prone, as mutating this object will affect any objects that use the defaults as a base.
@@ -23,8 +24,8 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
},
routes: {
account: '/account',
browseByFolder: '/browse-by-folder',
createFirstUser: '/create-first-user',
folders: '/folders',
forgot: '/forgot',
inactivity: '/logout-inactivity',
login: '/login',
@@ -99,8 +100,8 @@ export const addDefaultsToConfig = (config: Config): Config => {
},
routes: {
account: '/account',
browseByFolder: '/browse-by-folder',
createFirstUser: '/create-first-user',
folders: '/folders',
forgot: '/forgot',
inactivity: '/logout-inactivity',
login: '/login',
@@ -112,9 +113,9 @@ export const addDefaultsToConfig = (config: Config): Config => {
}
config.folders = {
collections: {},
slug: foldersSlug,
debug: false,
enabled: false,
fieldName: parentFolderFieldName,
...(config.folders || {}),
}

View File

@@ -40,10 +40,10 @@ import type {
import type { DatabaseAdapterResult } from '../database/types.js'
import type { EmailAdapter, SendEmailOptions } from '../email/types.js'
import type { ErrorName } from '../errors/types.js'
import type { RootFoldersConfiguration } from '../folders/types.js'
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
import type {
Block,
CollectionSlug,
FlattenedBlock,
JobsConfig,
Payload,
@@ -847,16 +847,16 @@ export type Config = {
* @default '/account'
*/
account?: `/${string}`
/** The route for the browse by folder view.
*
* @default '/browse-by-folder'
*/
browseByFolder: `/${string}`
/** The route for the create first user page.
*
* @default '/create-first-user'
*/
createFirstUser?: `/${string}`
/** The route for folder view.
*
* @default '/folders'
*/
folders: `/${string}`
/** The route for the forgot password page.
*
* @default '/forgot'
@@ -986,41 +986,7 @@ export type Config = {
/**
* Options for folder view within the admin panel
*/
folders?: {
/**
* An array of functions to be ran when the folder collection is initialized
* This allows plugins to modify the collection configuration
*/
collectionOverrides?: (({
collection,
}: {
collection: CollectionConfig
}) => CollectionConfig | Promise<CollectionConfig>)[]
/**
* Collections that you would like organize within folders
*/
collections: {
[key: CollectionSlug]: any
}
/**
* Ability to view hidden fields and collections related to folders
*
* @default false
*/
debug?: boolean
/**
* Enable folders in the admin panel
*
* @default false
*/
enabled?: boolean
/**
* Slug for the folder collection
*
* @default "_folders"
*/
slug?: string
}
folders?: RootFoldersConfiguration
/**
* @see https://payloadcms.com/docs/configuration/globals#global-configs
*/

View File

@@ -29,6 +29,7 @@ export {
fieldIsVirtual,
fieldShouldBeLocalized,
fieldSupportsMany,
groupHasName,
optionIsObject,
optionIsValue,
optionsAreObjects,

View File

@@ -770,7 +770,7 @@ export type NamedGroupFieldClient = {
export type UnnamedGroupFieldClient = {
admin?: AdminClient & Pick<UnnamedGroupField['admin'], 'hideGutter'>
fields: ClientField[]
} & Omit<FieldBaseClient, 'required'> &
} & Omit<FieldBaseClient, 'name' | 'required'> &
Pick<UnnamedGroupField, 'label' | 'type'>
export type GroupFieldClient = NamedGroupFieldClient | UnnamedGroupFieldClient
@@ -1960,6 +1960,12 @@ export function tabHasName<TField extends ClientTab | Tab>(tab: TField): tab is
return 'name' in tab
}
export function groupHasName(
group: Partial<NamedGroupFieldClient>,
): group is NamedGroupFieldClient {
return 'name' in group
}
/**
* Check if a field has localized: true set. This does not check if a field *should*
* be localized. To check if a field should be localized, use `fieldShouldBeLocalized`.

View File

@@ -1,7 +1,6 @@
import type { Config } from '../config/types.js'
import type { CollectionSlug } from '../index.js'
import { foldersSlug, parentFolderFieldName } from './constants.js'
import { createFolderCollection } from './createFolderCollection.js'
export async function addFolderCollections(config: NonNullable<Config>): Promise<void> {
@@ -9,51 +8,51 @@ export async function addFolderCollections(config: NonNullable<Config>): Promise
return
}
if (config.folders?.enabled) {
const enabledCollectionSlugs: CollectionSlug[] = []
const debug = Boolean(config.folders?.debug)
config.folders.slug = config.folders.slug || foldersSlug
const enabledCollectionSlugs: CollectionSlug[] = []
const debug = Boolean(config?.folders?.debug)
const folderFieldName = config?.folders?.fieldName as unknown as string
const folderSlug = config?.folders?.slug as unknown as CollectionSlug
for (let i = 0; i < config.collections.length; i++) {
const collection = config.collections[i]
if (config.folders.collections[collection.slug]) {
if (collection) {
collection.fields.push({
name: parentFolderFieldName,
type: 'relationship',
admin: {
allowCreate: false,
allowEdit: false,
components: {
Cell: '@payloadcms/ui/rsc#FolderTableCell',
Field: '@payloadcms/ui#FolderEditField',
},
for (let i = 0; i < config.collections.length; i++) {
const collection = config.collections[i]
if (collection?.admin?.folders) {
if (collection) {
collection.fields.push({
name: folderFieldName,
type: 'relationship',
admin: {
allowCreate: false,
allowEdit: false,
components: {
Cell: '@payloadcms/ui/rsc#FolderTableCell',
Field: '@payloadcms/ui/rsc#FolderEditField',
},
index: true,
label: 'Folder',
relationTo: config.folders.slug,
})
enabledCollectionSlugs.push(collection.slug)
}
},
index: true,
label: 'Folder',
relationTo: folderSlug,
})
enabledCollectionSlugs.push(collection.slug)
}
}
if (enabledCollectionSlugs.length) {
let folderCollection = createFolderCollection({
slug: config.folders.slug,
collectionSlugs: enabledCollectionSlugs,
debug,
})
if (
Array.isArray(config.folders.collectionOverrides) &&
config.folders.collectionOverrides.length
) {
for (const override of config.folders.collectionOverrides) {
folderCollection = await override({ collection: folderCollection })
}
}
config.collections.push(folderCollection)
}
}
if (enabledCollectionSlugs.length) {
let folderCollection = createFolderCollection({
slug: folderSlug,
collectionSlugs: enabledCollectionSlugs,
debug,
folderFieldName,
})
if (
Array.isArray(config?.folders?.collectionOverrides) &&
config?.folders.collectionOverrides.length
) {
for (const override of config.folders.collectionOverrides) {
folderCollection = await override({ collection: folderCollection })
}
}
config.collections.push(folderCollection)
}
}

View File

@@ -1,2 +1,2 @@
export const foldersSlug = '_folders'
export const parentFolderFieldName = '_folder'
export const foldersSlug = 'payload-folders'
export const parentFolderFieldName = 'folder'

View File

@@ -1,6 +1,5 @@
import type { CollectionConfig } from '../collections/config/types.js'
import { parentFolderFieldName } from './constants.js'
import { populateFolderDataEndpoint } from './endpoints/populateFolderData.js'
import { deleteSubfoldersAfterDelete } from './hooks/deleteSubfoldersAfterDelete.js'
import { dissasociateAfterDelete } from './hooks/dissasociateAfterDelete.js'
@@ -9,12 +8,14 @@ import { reparentChildFolder } from './hooks/reparentChildFolder.js'
type CreateFolderCollectionArgs = {
collectionSlugs: string[]
debug?: boolean
folderFieldName: string
slug: string
}
export const createFolderCollection = ({
slug,
collectionSlugs,
debug,
folderFieldName,
}: CreateFolderCollectionArgs): CollectionConfig => ({
slug,
admin: {
@@ -30,7 +31,7 @@ export const createFolderCollection = ({
required: true,
},
{
name: parentFolderFieldName,
name: folderFieldName,
type: 'relationship',
admin: {
hidden: !debug,
@@ -46,21 +47,21 @@ export const createFolderCollection = ({
},
collection: [slug, ...collectionSlugs],
hasMany: true,
on: parentFolderFieldName,
on: folderFieldName,
},
],
hooks: {
afterChange: [
reparentChildFolder({
parentFolderFieldName,
folderFieldName,
}),
],
afterDelete: [
dissasociateAfterDelete({
collectionSlugs,
parentFolderFieldName,
folderFieldName,
}),
deleteSubfoldersAfterDelete({ folderSlug: slug, parentFolderFieldName }),
deleteSubfoldersAfterDelete({ folderFieldName, folderSlug: slug }),
],
},
labels: {

View File

@@ -1,19 +1,19 @@
import type { CollectionAfterDeleteHook } from '../../index.js'
type Args = {
folderFieldName: string
folderSlug: string
parentFolderFieldName: string
}
export const deleteSubfoldersAfterDelete = ({
folderFieldName,
folderSlug,
parentFolderFieldName,
}: Args): CollectionAfterDeleteHook => {
return async ({ id, req }) => {
await req.payload.delete({
collection: folderSlug,
req,
where: {
[parentFolderFieldName]: {
[folderFieldName]: {
equals: id,
},
},

View File

@@ -2,22 +2,22 @@ import type { CollectionAfterDeleteHook } from '../../index.js'
type Args = {
collectionSlugs: string[]
parentFolderFieldName: string
folderFieldName: string
}
export const dissasociateAfterDelete = ({
collectionSlugs,
parentFolderFieldName,
folderFieldName,
}: Args): CollectionAfterDeleteHook => {
return async ({ id, req }) => {
for (const collectionSlug of collectionSlugs) {
await req.payload.update({
collection: collectionSlug,
data: {
[parentFolderFieldName]: null,
[folderFieldName]: null,
},
req,
where: {
[parentFolderFieldName]: {
[folderFieldName]: {
equals: id,
},
},

View File

@@ -3,8 +3,8 @@ import type { CollectionAfterChangeHook, Payload } from '../../index.js'
import { extractID } from '../../utilities/extractID.js'
type Args = {
folderFieldName: string
folderID: number | string
parentFolderFieldName: string
parentIDToFind: number | string
payload: Payload
}
@@ -14,8 +14,8 @@ type Args = {
* recursively checking upwards through the folder hierarchy.
*/
async function isChildOfFolder({
folderFieldName,
folderID,
parentFolderFieldName,
parentIDToFind,
payload,
}: Args): Promise<boolean> {
@@ -24,8 +24,8 @@ async function isChildOfFolder({
collection: payload.config.folders.slug,
})
const parentFolderID = parentFolder[parentFolderFieldName]
? extractID(parentFolder[parentFolderFieldName])
const parentFolderID = parentFolder[folderFieldName]
? extractID(parentFolder[folderFieldName])
: undefined
if (!parentFolderID) {
@@ -39,8 +39,8 @@ async function isChildOfFolder({
}
return isChildOfFolder({
folderFieldName,
folderID: parentFolderID,
parentFolderFieldName,
parentIDToFind,
payload,
})
@@ -66,20 +66,17 @@ async function isChildOfFolder({
```
*/
export const reparentChildFolder = ({
parentFolderFieldName,
folderFieldName,
}: {
parentFolderFieldName: string
folderFieldName: string
}): CollectionAfterChangeHook => {
return async ({ doc, previousDoc, req }) => {
if (
previousDoc[parentFolderFieldName] !== doc[parentFolderFieldName] &&
doc[parentFolderFieldName]
) {
const newParentFolderID = extractID(doc[parentFolderFieldName])
if (previousDoc[folderFieldName] !== doc[folderFieldName] && doc[folderFieldName]) {
const newParentFolderID = extractID(doc[folderFieldName])
const isMovingToChild = newParentFolderID
? await isChildOfFolder({
folderFieldName,
folderID: newParentFolderID,
parentFolderFieldName,
parentIDToFind: doc.id,
payload: req.payload,
})
@@ -92,8 +89,8 @@ export const reparentChildFolder = ({
id: newParentFolderID,
collection: req.payload.config.folders.slug,
data: {
[parentFolderFieldName]: previousDoc[parentFolderFieldName]
? extractID(previousDoc[parentFolderFieldName])
[folderFieldName]: previousDoc[folderFieldName]
? extractID(previousDoc[folderFieldName])
: null,
},
req,

View File

@@ -1,15 +1,15 @@
import type { TypeWithID } from '../collections/config/types.js'
import type { CollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { CollectionSlug, SanitizedCollectionConfig } from '../index.js'
import type { Document } from '../types/index.js'
export type FolderInterface = {
_folder?: FolderInterface | (number | string | undefined)
documentsAndFolders?: {
docs: {
relationTo: CollectionSlug
value: Document
}[]
}
folder?: FolderInterface | (number | string | undefined)
name: string
} & TypeWithID
@@ -55,9 +55,9 @@ export type FolderOrDocument = {
itemKey: FolderDocumentItemKey
relationTo: CollectionSlug
value: {
_folder?: number | string
_folderOrDocumentTitle: string
createdAt?: string
folderID?: number | string
id: number | string
updatedAt?: string
} & DocumentMediaData
@@ -68,3 +68,35 @@ export type GetFolderDataResult = {
documents: FolderOrDocument[]
subfolders: FolderOrDocument[]
}
export type RootFoldersConfiguration = {
/**
* An array of functions to be ran when the folder collection is initialized
* This allows plugins to modify the collection configuration
*/
collectionOverrides?: (({
collection,
}: {
collection: CollectionConfig
}) => CollectionConfig | Promise<CollectionConfig>)[]
/**
* Ability to view hidden fields and collections related to folders
*
* @default false
*/
debug?: boolean
/**
* The Folder field name
*
* @default "folder"
*/
fieldName?: string
/**
* Slug for the folder collection
*
* @default "payload-folders"
*/
slug?: string
}
export type CollectionFoldersConfiguration = boolean

View File

@@ -2,12 +2,14 @@ import type { CollectionSlug, Document } from '../../index.js'
import type { FolderOrDocument } from '../types.js'
type Args = {
folderFieldName: string
isUpload: boolean
relationTo: CollectionSlug
useAsTitle?: string
value: Document
}
export function formatFolderOrDocumentItem({
folderFieldName,
isUpload,
relationTo,
useAsTitle,
@@ -15,9 +17,9 @@ export function formatFolderOrDocumentItem({
}: Args): FolderOrDocument {
const itemValue: FolderOrDocument['value'] = {
id: value?.id,
_folder: value?._folder,
_folderOrDocumentTitle: value[useAsTitle || 'id'],
_folderOrDocumentTitle: (useAsTitle && value?.[useAsTitle]) || value['id'],
createdAt: value?.createdAt,
folderID: value?.[folderFieldName],
updatedAt: value?.updatedAt,
}

View File

@@ -1,9 +1,7 @@
import type { PaginatedDocs } from '../../database/types.js'
import type { User } from '../../index.js'
import type { Payload } from '../../types/index.js'
import type { FolderBreadcrumb, FolderInterface } from '../types.js'
import type { Document, Payload } from '../../types/index.js'
import type { FolderBreadcrumb } from '../types.js'
import { parentFolderFieldName } from '../constants.js'
type GetFolderBreadcrumbsArgs = {
breadcrumbs?: FolderBreadcrumb[]
folderID?: number | string
@@ -20,15 +18,16 @@ export const getFolderBreadcrumbs = async ({
payload,
user,
}: GetFolderBreadcrumbsArgs): Promise<FolderBreadcrumb[] | null> => {
const folderFieldName: string = payload.config.folders.fieldName
if (folderID) {
const folderQuery = (await payload.find({
const folderQuery = await payload.find({
collection: payload.config.folders.slug,
depth: 0,
limit: 1,
overrideAccess: false,
select: {
name: true,
[parentFolderFieldName]: true,
[folderFieldName]: true,
},
user,
where: {
@@ -36,23 +35,23 @@ export const getFolderBreadcrumbs = async ({
equals: folderID,
},
},
})) as PaginatedDocs<FolderInterface>
})
const folder = folderQuery.docs[0]
const folder = folderQuery.docs[0] as Document
if (folder) {
breadcrumbs.push({
id: folder.id,
name: folder.name,
})
if (folder[parentFolderFieldName]) {
if (folder[folderFieldName]) {
return getFolderBreadcrumbs({
breadcrumbs,
folderID:
typeof folder[parentFolderFieldName] === 'number' ||
typeof folder[parentFolderFieldName] === 'string'
? folder[parentFolderFieldName]
: folder[parentFolderFieldName].id,
typeof folder[folderFieldName] === 'number' ||
typeof folder[folderFieldName] === 'string'
? folder[folderFieldName]
: folder[folderFieldName].id,
payload,
user,
})

View File

@@ -22,6 +22,16 @@ export async function queryDocumentsAndFoldersFromJoin({
payload,
user,
}: QueryDocumentsAndFoldersArgs): Promise<QueryDocumentsAndFoldersResults> {
const folderCollectionSlugs: string[] = payload.config.collections.reduce<string[]>(
(acc, collection) => {
if (collection?.admin?.folders) {
acc.push(collection.slug)
}
return acc
},
[],
)
const subfolderDoc = (await payload.find({
collection: payload.config.folders.slug,
joins: {
@@ -32,16 +42,14 @@ export async function queryDocumentsAndFoldersFromJoin({
relationTo: {
in: [
payload.config.folders.slug,
...(collectionSlug
? [collectionSlug]
: Object.keys(payload.config.folders.collections)),
...(collectionSlug ? [collectionSlug] : folderCollectionSlugs),
],
},
},
},
},
limit: 1,
// overrideAccess: false, // @todo: bug in core, throws "QueryError: The following paths cannot be queried: relationTo"
overrideAccess: false,
user,
where: {
id: {
@@ -56,6 +64,7 @@ export async function queryDocumentsAndFoldersFromJoin({
(acc: QueryDocumentsAndFoldersResults, doc: Document) => {
const { relationTo, value } = doc
const item = formatFolderOrDocumentItem({
folderFieldName: payload.config.folders.fieldName,
isUpload: Boolean(payload.collections[relationTo].config.upload),
relationTo,
useAsTitle: payload.collections[relationTo].config.admin?.useAsTitle,

View File

@@ -18,12 +18,12 @@ export async function getOrphanedDocs({
let whereConstraints: Where = {
or: [
{
_folder: {
[payload.config.folders.fieldName]: {
exists: false,
},
},
{
_folder: {
[payload.config.folders.fieldName]: {
equals: null,
},
},
@@ -41,7 +41,7 @@ export async function getOrphanedDocs({
const orphanedFolders = await payload.find({
collection: collectionSlug,
limit: 0,
// overrideAccess: false, // @todo: bug in core, throws "QueryError: The following paths cannot be queried: _folder"
overrideAccess: false,
sort: payload.collections[collectionSlug].config.admin.useAsTitle,
user,
where: whereConstraints,
@@ -50,6 +50,7 @@ export async function getOrphanedDocs({
return (
orphanedFolders?.docs.map((doc) =>
formatFolderOrDocumentItem({
folderFieldName: payload.config.folders.fieldName,
isUpload: Boolean(payload.collections[collectionSlug].config.upload),
relationTo: collectionSlug,
useAsTitle: payload.collections[collectionSlug].config.admin.useAsTitle,

View File

@@ -65,10 +65,12 @@ export const getConstraints = (config: Config): Field => ({
hooks: {
beforeChange: [
({ data, req }) => {
if (data?.access?.[operation]?.constraint === 'onlyMe') {
if (req.user) {
return [req.user.id]
}
if (data?.access?.[operation]?.constraint === 'onlyMe' && req.user) {
return [req.user.id]
}
if (data?.access?.[operation]?.constraint === 'specificUsers' && req.user) {
return [...(data?.access?.[operation]?.users || []), req.user.id]
}
return data?.access?.[operation]?.users

View File

@@ -72,7 +72,7 @@ export const preventLockout: Validate = async (
canUpdate = true
} catch (_err) {
if (!canRead || !canUpdate) {
throw new APIError('Cannot remove yourself from this preset.', 403, {}, true)
throw new APIError('This action will lock you out of this preset.', 403, {}, true)
}
} finally {
if (transaction) {

View File

@@ -43,6 +43,33 @@ export async function cropImage({
sharpOptions.animated = true
}
const { height: originalHeight, width: originalWidth } = dimensions
const newWidth = Number(widthInPixels)
const newHeight = Number(heightInPixels)
const dimensionsChanged = originalWidth !== newWidth || originalHeight !== newHeight
if (!dimensionsChanged) {
let adjustedHeight = originalHeight
if (fileIsAnimatedType) {
const animatedMetadata = await sharp(
file.tempFilePath || file.data,
sharpOptions,
).metadata()
adjustedHeight = animatedMetadata.pages ? animatedMetadata.height : originalHeight
}
return {
data: file.data,
info: {
height: adjustedHeight,
size: file.size,
width: originalWidth,
},
}
}
const formattedCropData = {
height: Number(heightInPixels),
left: percentToPixel(x, dimensions.width),

View File

@@ -241,7 +241,7 @@ export const generateFileData = async <T>({
})
// Apply resize after cropping to ensure it conforms to resizeOptions
if (resizeOptions) {
if (resizeOptions && !resizeOptions.withoutEnlargement) {
const resizedAfterCrop = await sharp(croppedImage)
.resize({
fit: resizeOptions?.fit || 'cover',

View File

@@ -0,0 +1,510 @@
import { I18nClient } from '@payloadcms/translations'
import { ClientField } from '../fields/config/client.js'
import flattenFields from './flattenTopLevelFields.js'
describe('flattenFields', () => {
const i18n: I18nClient = {
t: (value: string) => value,
language: 'en',
dateFNS: {} as any,
dateFNSKey: 'en-US',
fallbackLanguage: 'en',
translations: {},
}
const baseField: ClientField = {
type: 'text',
name: 'title',
label: 'Title',
}
describe('basic flattening', () => {
it('should return flat list for top-level fields', () => {
const fields = [baseField]
const result = flattenFields(fields)
expect(result).toHaveLength(1)
expect(result[0].name).toBe('title')
})
})
describe('group flattening', () => {
it('should flatten fields inside group with accessor and labelWithPrefix with moveSubFieldsToTop', () => {
const fields: ClientField[] = [
{
type: 'group',
name: 'meta',
label: 'Meta Info',
fields: [
{
type: 'text',
name: 'slug',
label: 'Slug',
},
],
},
]
const result = flattenFields(fields, {
moveSubFieldsToTop: true,
i18n,
})
expect(result).toHaveLength(1)
expect(result[0].name).toBe('slug')
expect(result[0].accessor).toBe('meta-slug')
expect(result[0].labelWithPrefix).toBe('Meta Info > Slug')
})
it('should NOT flatten fields inside group without moveSubFieldsToTop', () => {
const fields: ClientField[] = [
{
type: 'group',
name: 'meta',
label: 'Meta Info',
fields: [
{
type: 'text',
name: 'slug',
label: 'Slug',
},
],
},
]
const result = flattenFields(fields)
// Should return the group as a top-level item, not the inner field
expect(result).toHaveLength(1)
expect(result[0].name).toBe('meta')
expect('fields' in result[0]).toBe(true)
expect('accessor' in result[0]).toBe(false)
expect('labelWithPrefix' in result[0]).toBe(false)
})
it('should correctly handle deeply nested group fields with and without moveSubFieldsToTop', () => {
const fields: ClientField[] = [
{
type: 'group',
name: 'outer',
label: 'Outer',
fields: [
{
type: 'group',
name: 'inner',
label: 'Inner',
fields: [
{
type: 'text',
name: 'deep',
label: 'Deep Field',
},
],
},
],
},
]
const hoisted = flattenFields(fields, {
moveSubFieldsToTop: true,
i18n,
})
expect(hoisted).toHaveLength(1)
expect(hoisted[0].name).toBe('deep')
expect(hoisted[0].accessor).toBe('outer-inner-deep')
expect(hoisted[0].labelWithPrefix).toBe('Outer > Inner > Deep Field')
const nonHoisted = flattenFields(fields)
expect(nonHoisted).toHaveLength(1)
expect(nonHoisted[0].name).toBe('outer')
expect('fields' in nonHoisted[0]).toBe(true)
expect('accessor' in nonHoisted[0]).toBe(false)
expect('labelWithPrefix' in nonHoisted[0]).toBe(false)
})
it('should hoist fields from unnamed group if moveSubFieldsToTop is true', () => {
const fields: ClientField[] = [
{
type: 'group',
label: 'Unnamed group',
fields: [
{
type: 'text',
name: 'insideUnnamedGroup',
},
],
},
]
const withExtract = flattenFields(fields, {
moveSubFieldsToTop: true,
i18n,
})
// Should keep the group as a single top-level field
expect(withExtract).toHaveLength(1)
expect(withExtract[0].type).toBe('text')
expect(withExtract[0].accessor).toBeUndefined()
expect(withExtract[0].labelWithPrefix).toBeUndefined()
const withoutExtract = flattenFields(fields)
expect(withoutExtract).toHaveLength(1)
expect(withoutExtract[0].type).toBe('group')
expect(withoutExtract[0].accessor).toBeUndefined()
expect(withoutExtract[0].labelWithPrefix).toBeUndefined()
})
it('should hoist using deepest named group only if parents are unnamed', () => {
const fields: ClientField[] = [
{
type: 'group',
label: 'Outer',
fields: [
{
type: 'group',
label: 'Middle',
fields: [
{
type: 'group',
name: 'namedGroup',
label: 'Named Group',
fields: [
{
type: 'group',
label: 'Inner',
fields: [
{
type: 'text',
name: 'nestedField',
label: 'Nested Field',
},
],
},
],
},
],
},
],
},
]
const hoistedResult = flattenFields(fields, {
moveSubFieldsToTop: true,
i18n,
})
expect(hoistedResult).toHaveLength(1)
expect(hoistedResult[0].name).toBe('nestedField')
expect(hoistedResult[0].accessor).toBe('namedGroup-nestedField')
expect(hoistedResult[0].labelWithPrefix).toBe('Named Group > Nested Field')
const nonHoistedResult = flattenFields(fields)
expect(nonHoistedResult).toHaveLength(1)
expect(nonHoistedResult[0].type).toBe('group')
expect('fields' in nonHoistedResult[0]).toBe(true)
expect('accessor' in nonHoistedResult[0]).toBe(false)
expect('labelWithPrefix' in nonHoistedResult[0]).toBe(false)
})
})
describe('array and block edge cases', () => {
it('should NOT flatten fields in arrays or blocks with moveSubFieldsToTop', () => {
const fields: ClientField[] = [
{
type: 'array',
name: 'items',
label: 'Items',
fields: [
{
type: 'text',
name: 'label',
label: 'Label',
},
],
},
{
type: 'blocks',
name: 'layout',
blocks: [
{
slug: 'block',
fields: [
{
type: 'text',
name: 'content',
label: 'Content',
},
],
},
],
},
]
const result = flattenFields(fields, { moveSubFieldsToTop: true })
expect(result).toHaveLength(2)
expect(result[0].name).toBe('items')
expect(result[1].name).toBe('layout')
})
it('should NOT flatten fields in arrays or blocks without moveSubFieldsToTop', () => {
const fields: ClientField[] = [
{
type: 'array',
name: 'things',
label: 'Things',
fields: [
{
type: 'text',
name: 'thingLabel',
label: 'Thing Label',
},
],
},
{
type: 'blocks',
name: 'contentBlocks',
blocks: [
{
slug: 'content',
fields: [
{
type: 'text',
name: 'body',
label: 'Body',
},
],
},
],
},
]
const result = flattenFields(fields)
expect(result).toHaveLength(2)
expect(result[0].name).toBe('things')
expect(result[1].name).toBe('contentBlocks')
})
it('should not hoist group fields nested inside arrays', () => {
const fields: ClientField[] = [
{
type: 'array',
name: 'arrayField',
label: 'Array Field',
fields: [
{
type: 'group',
name: 'groupInArray',
label: 'Group In Array',
fields: [
{
type: 'text',
name: 'nestedInArrayGroup',
label: 'Nested In Array Group',
},
],
},
],
},
]
const result = flattenFields(fields, { moveSubFieldsToTop: true })
expect(result).toHaveLength(1)
expect(result[0].name).toBe('arrayField')
})
it('should not hoist group fields nested inside blocks', () => {
const fields: ClientField[] = [
{
type: 'blocks',
name: 'blockField',
blocks: [
{
slug: 'exampleBlock',
fields: [
{
type: 'group',
name: 'groupInBlock',
label: 'Group In Block',
fields: [
{
type: 'text',
name: 'nestedInBlockGroup',
label: 'Nested In Block Group',
},
],
},
],
},
],
},
]
const result = flattenFields(fields, { moveSubFieldsToTop: true })
expect(result).toHaveLength(1)
expect(result[0].name).toBe('blockField')
})
})
describe('row and collapsible behavior', () => {
it('should recursively flatten collapsible fields regardless of moveSubFieldsToTop', () => {
const fields: ClientField[] = [
{
type: 'collapsible',
label: 'Collapsible',
fields: [
{
type: 'text',
name: 'nickname',
label: 'Nickname',
},
],
},
]
const defaultResult = flattenFields(fields)
const hoistedResult = flattenFields(fields, { moveSubFieldsToTop: true })
for (const result of [defaultResult, hoistedResult]) {
expect(result).toHaveLength(1)
expect(result[0].name).toBe('nickname')
expect('accessor' in result[0]).toBe(false)
expect('labelWithPrefix' in result[0]).toBe(false)
}
})
it('should recursively flatten row fields regardless of moveSubFieldsToTop', () => {
const fields: ClientField[] = [
{
type: 'row',
fields: [
{
type: 'text',
name: 'firstName',
label: 'First Name',
},
{
type: 'text',
name: 'lastName',
label: 'Last Name',
},
],
},
]
const defaultResult = flattenFields(fields)
const hoistedResult = flattenFields(fields, { moveSubFieldsToTop: true })
for (const result of [defaultResult, hoistedResult]) {
expect(result).toHaveLength(2)
expect(result[0].name).toBe('firstName')
expect(result[1].name).toBe('lastName')
expect('accessor' in result[0]).toBe(false)
expect('labelWithPrefix' in result[0]).toBe(false)
}
})
it('should hoist named group fields inside rows', () => {
const fields: ClientField[] = [
{
type: 'row',
fields: [
{
type: 'group',
name: 'groupInRow',
label: 'Group In Row',
fields: [
{
type: 'text',
name: 'nestedInRowGroup',
label: 'Nested In Row Group',
},
],
},
],
},
]
const result = flattenFields(fields, {
moveSubFieldsToTop: true,
i18n,
})
expect(result).toHaveLength(1)
expect(result[0].accessor).toBe('groupInRow-nestedInRowGroup')
expect(result[0].labelWithPrefix).toBe('Group In Row > Nested In Row Group')
})
it('should hoist named group fields inside collapsibles', () => {
const fields: ClientField[] = [
{
type: 'collapsible',
label: 'Collapsible',
fields: [
{
type: 'group',
name: 'groupInCollapsible',
label: 'Group In Collapsible',
fields: [
{
type: 'text',
name: 'nestedInCollapsibleGroup',
label: 'Nested In Collapsible Group',
},
],
},
],
},
]
const result = flattenFields(fields, {
moveSubFieldsToTop: true,
i18n,
})
expect(result).toHaveLength(1)
expect(result[0].accessor).toBe('groupInCollapsible-nestedInCollapsibleGroup')
expect(result[0].labelWithPrefix).toBe('Group In Collapsible > Nested In Collapsible Group')
})
})
describe('tab integration', () => {
it('should hoist named group fields inside tabs when moveSubFieldsToTop is true', () => {
const fields: ClientField[] = [
{
type: 'tabs',
tabs: [
{
label: 'Tab One',
fields: [
{
type: 'group',
name: 'groupInTab',
label: 'Group In Tab',
fields: [
{
type: 'text',
name: 'nestedInTabGroup',
label: 'Nested In Tab Group',
},
],
},
],
},
],
},
]
const result = flattenFields(fields, {
moveSubFieldsToTop: true,
i18n,
})
expect(result).toHaveLength(1)
expect(result[0].accessor).toBe('groupInTab-nestedInTabGroup')
expect(result[0].labelWithPrefix).toBe('Group In Tab > Nested In Tab Group')
})
})
})

View File

@@ -1,4 +1,8 @@
// @ts-strict-ignore
import type { I18nClient } from '@payloadcms/translations'
import { getTranslation } from '@payloadcms/translations'
import type { ClientTab } from '../admin/fields/Tabs.js'
import type { ClientField } from '../fields/config/client.js'
import type {
@@ -18,38 +22,153 @@ import {
} from '../fields/config/types.js'
type FlattenedField<TField> = TField extends ClientField
? FieldAffectingDataClient | FieldPresentationalOnlyClient
: FieldAffectingData | FieldPresentationalOnly
? { accessor?: string; labelWithPrefix?: string } & (
| FieldAffectingDataClient
| FieldPresentationalOnlyClient
)
: { accessor?: string; labelWithPrefix?: string } & (FieldAffectingData | FieldPresentationalOnly)
type TabType<TField> = TField extends ClientField ? ClientTab : Tab
/**
* Flattens a collection's fields into a single array of fields, as long
* as the fields do not affect data.
* Options to control how fields are flattened.
*/
type FlattenFieldsOptions = {
/**
* i18n context used for translating `label` values via `getTranslation`.
*/
i18n?: I18nClient
/**
* If true, presentational-only fields (like UI fields) will be included
* in the output. Otherwise, they will be skipped.
* Default: false.
*/
keepPresentationalFields?: boolean
/**
* A label prefix to prepend to translated labels when building `labelWithPrefix`.
* Used recursively when flattening nested fields.
*/
labelPrefix?: string
/**
* If true, nested fields inside `group` fields will be lifted to the top level
* and given contextual `accessor` and `labelWithPrefix` values.
* Default: false.
*/
moveSubFieldsToTop?: boolean
/**
* A path prefix to prepend to field names when building the `accessor`.
* Used recursively when flattening nested fields.
*/
pathPrefix?: string
}
/**
* Flattens a collection's fields into a single array of fields, optionally
* extracting nested fields in group fields.
*
* @param fields
* @param keepPresentationalFields if true, will skip flattening fields that are presentational only
* @param fields - Array of fields to flatten
* @param options - Options to control the flattening behavior
*/
function flattenFields<TField extends ClientField | Field>(
fields: TField[],
keepPresentationalFields?: boolean,
options?: boolean | FlattenFieldsOptions,
): FlattenedField<TField>[] {
const normalizedOptions: FlattenFieldsOptions =
typeof options === 'boolean' ? { keepPresentationalFields: options } : (options ?? {})
const {
i18n,
keepPresentationalFields,
labelPrefix,
moveSubFieldsToTop = false,
pathPrefix,
} = normalizedOptions
return fields.reduce<FlattenedField<TField>[]>((acc, field) => {
if (fieldAffectsData(field) || (keepPresentationalFields && fieldIsPresentationalOnly(field))) {
acc.push(field as FlattenedField<TField>)
} else if (fieldHasSubFields(field)) {
acc.push(...flattenFields(field.fields as TField[], keepPresentationalFields))
if (fieldHasSubFields(field)) {
if (field.type === 'group') {
if (moveSubFieldsToTop && 'fields' in field) {
const isNamedGroup = 'name' in field && typeof field.name === 'string' && !!field.name
const translatedLabel =
'label' in field && field.label && i18n
? getTranslation(field.label as string, i18n)
: undefined
const labelWithPrefix =
isNamedGroup && labelPrefix && translatedLabel
? `${labelPrefix} > ${translatedLabel}`
: (labelPrefix ?? translatedLabel)
const nameWithPrefix =
isNamedGroup && field.name
? pathPrefix
? `${pathPrefix}-${field.name as string}`
: (field.name as string)
: pathPrefix
acc.push(
...flattenFields(field.fields as TField[], {
i18n,
keepPresentationalFields,
labelPrefix: isNamedGroup ? labelWithPrefix : labelPrefix,
moveSubFieldsToTop,
pathPrefix: isNamedGroup ? nameWithPrefix : pathPrefix,
}),
)
} else {
// Just keep the group as-is
acc.push(field as FlattenedField<TField>)
}
} else if (['collapsible', 'row'].includes(field.type)) {
// Recurse into row and collapsible
acc.push(...flattenFields(field.fields as TField[], options))
} else {
// Do not hoist fields from arrays & blocks
acc.push(field as FlattenedField<TField>)
}
} else if (
fieldAffectsData(field) ||
(keepPresentationalFields && fieldIsPresentationalOnly(field))
) {
// Ignore nested `id` fields when inside nested structure
if (field.name === 'id' && labelPrefix !== undefined) {
return acc
}
const translatedLabel =
'label' in field && field.label && i18n ? getTranslation(field.label, i18n) : undefined
const name = 'name' in field ? field.name : undefined
const isHoistingFromGroup = pathPrefix !== undefined || labelPrefix !== undefined
acc.push({
...(field as FlattenedField<TField>),
...(moveSubFieldsToTop &&
isHoistingFromGroup && {
accessor: pathPrefix && name ? `${pathPrefix}-${name}` : (name ?? ''),
labelWithPrefix:
labelPrefix && translatedLabel
? `${labelPrefix} > ${translatedLabel}`
: (labelPrefix ?? translatedLabel),
}),
})
} else if (field.type === 'tabs' && 'tabs' in field) {
return [
...acc,
...field.tabs.reduce<FlattenedField<TField>[]>((tabFields, tab: TabType<TField>) => {
if (tabHasName(tab)) {
return [...tabFields, { ...tab, type: 'tab' } as unknown as FlattenedField<TField>]
} else {
return [
...tabFields,
...flattenFields(tab.fields as TField[], keepPresentationalFields),
{
...tab,
type: 'tab',
...(moveSubFieldsToTop && { labelPrefix }),
} as unknown as FlattenedField<TField>,
]
} else {
return [...tabFields, ...flattenFields<TField>(tab.fields as TField[], options)]
}
}, []),
]

View File

@@ -47,36 +47,36 @@ let baseEvent: BaseEvent | null = null
export const sendEvent = async ({ event, payload }: Args): Promise<void> => {
try {
const { packageJSON, packageJSONPath } = await getPackageJSON()
// Only generate the base event once
if (!baseEvent) {
const { projectID, source: projectIDSource } = getProjectID(payload, packageJSON)
baseEvent = {
ciName: ciInfo.isCI ? ciInfo.name : null,
envID: getEnvID(),
isCI: ciInfo.isCI,
nodeEnv: process.env.NODE_ENV || 'development',
nodeVersion: process.version,
payloadVersion: getPayloadVersion(packageJSON),
projectID,
projectIDSource,
...getLocalizationInfo(payload),
dbAdapter: payload.db.name,
emailAdapter: payload.email?.name || null,
uploadAdapters: payload.config.upload.adapters,
}
}
if (process.env.PAYLOAD_TELEMETRY_DEBUG) {
payload.logger.info({
event: { ...baseEvent, ...event, packageJSONPath },
msg: 'Telemetry Event',
})
return
}
if (payload.config.telemetry !== false) {
const { packageJSON, packageJSONPath } = await getPackageJSON()
// Only generate the base event once
if (!baseEvent) {
const { projectID, source: projectIDSource } = getProjectID(payload, packageJSON)
baseEvent = {
ciName: ciInfo.isCI ? ciInfo.name : null,
envID: getEnvID(),
isCI: ciInfo.isCI,
nodeEnv: process.env.NODE_ENV || 'development',
nodeVersion: process.version,
payloadVersion: getPayloadVersion(packageJSON),
projectID,
projectIDSource,
...getLocalizationInfo(payload),
dbAdapter: payload.db.name,
emailAdapter: payload.email?.name || null,
uploadAdapters: payload.config.upload.adapters,
}
}
if (process.env.PAYLOAD_TELEMETRY_DEBUG) {
payload.logger.info({
event: { ...baseEvent, ...event, packageJSONPath },
msg: 'Telemetry Event',
})
return
}
await fetch('https://telemetry.payloadcms.com/events', {
body: JSON.stringify({ ...baseEvent, ...event }),
headers: {

View File

@@ -487,6 +487,55 @@ const Checkbox: Block = {
},
}
const Date: Block = {
slug: 'date',
fields: [
{
type: 'row',
fields: [
{
...name,
admin: {
width: '50%',
},
},
{
...label,
admin: {
width: '50%',
},
},
],
},
{
type: 'row',
fields: [
{
...width,
admin: {
width: '50%',
},
},
{
...required,
admin: {
width: '50%',
},
},
],
},
{
name: 'defaultValue',
type: 'date',
label: 'Default Value',
},
],
labels: {
plural: 'Date Fields',
singular: 'Date',
},
}
const Payment = (fieldConfig: PaymentFieldConfig): Block => {
let paymentProcessorField = null
if (fieldConfig?.paymentProcessor) {
@@ -669,6 +718,7 @@ const Message: Block = {
export const fields = {
checkbox: Checkbox,
country: Country,
date: Date,
email: Email,
message: Message,
number: Number,

View File

@@ -33,6 +33,7 @@ export interface FieldsConfig {
[key: string]: boolean | FieldConfig | undefined
checkbox?: boolean | FieldConfig
country?: boolean | FieldConfig
date?: boolean | FieldConfig
email?: boolean | FieldConfig
message?: boolean | FieldConfig
number?: boolean | FieldConfig
@@ -146,6 +147,16 @@ export interface EmailField {
width?: number
}
export interface DateField {
blockName?: string
blockType: 'date'
defaultValue?: string
label?: string
name: string
required?: boolean
width?: number
}
export interface StateField {
blockName?: string
blockType: 'state'
@@ -185,6 +196,7 @@ export interface MessageField {
export type FormFieldBlock =
| CheckboxField
| CountryField
| DateField
| EmailField
| MessageField
| PaymentField

View File

@@ -42,6 +42,16 @@
"import": "./src/exports/rsc.ts",
"types": "./src/exports/rsc.ts",
"default": "./src/exports/rsc.ts"
},
"./translations/languages/all": {
"import": "./src/translations/index.ts",
"types": "./src/translations/index.ts",
"default": "./src/translations/index.ts"
},
"./translations/languages/*": {
"import": "./src/translations/languages/*.ts",
"types": "./src/translations/languages/*.ts",
"default": "./src/translations/languages/*.ts"
}
},
"main": "./src/index.ts",
@@ -92,6 +102,16 @@
"import": "./dist/exports/rsc.js",
"types": "./dist/exports/rsc.d.ts",
"default": "./dist/exports/rsc.js"
},
"./translations/languages/all": {
"import": "./dist/translations/index.js",
"types": "./dist/translations/index.d.ts",
"default": "./dist/translations/index.js"
},
"./translations/languages/*": {
"import": "./dist/translations/languages/*.js",
"types": "./dist/translations/languages/*.d.ts",
"default": "./dist/translations/languages/*.js"
}
},
"main": "./dist/index.js",

View File

@@ -1,9 +1,20 @@
'use client'
import { getTranslation } from '@payloadcms/translations'
import { PopupList, useConfig, useDocumentDrawer, useTranslation } from '@payloadcms/ui'
import {
PopupList,
Translation,
useConfig,
useDocumentDrawer,
useTranslation,
} from '@payloadcms/ui'
import React, { useEffect } from 'react'
import type {
PluginImportExportTranslationKeys,
PluginImportExportTranslations,
} from '../../translations/index.js'
import { useImportExport } from '../ImportExportProvider/index.js'
import './index.scss'
@@ -14,7 +25,10 @@ export const ExportListMenuItem: React.FC<{
exportCollectionSlug: string
}> = ({ collectionSlug, exportCollectionSlug }) => {
const { getEntityConfig } = useConfig()
const { i18n } = useTranslation()
const { i18n, t } = useTranslation<
PluginImportExportTranslations,
PluginImportExportTranslationKeys
>()
const currentCollectionConfig = getEntityConfig({ collectionSlug })
const [DocumentDrawer, DocumentDrawerToggler] = useDocumentDrawer({
@@ -30,7 +44,15 @@ export const ExportListMenuItem: React.FC<{
return (
<PopupList.Button className={baseClass}>
<DocumentDrawerToggler>
Export {getTranslation(currentCollectionConfig.labels.plural, i18n)}
<Translation
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
i18nKey="plugin-import-export:exportDocumentLabel"
t={t}
variables={{
label: getTranslation(currentCollectionConfig.labels.plural, i18n),
}}
/>
</DocumentDrawerToggler>
<DocumentDrawer />
</PopupList.Button>

View File

@@ -1,10 +1,15 @@
'use client'
import { Button, SaveButton, useConfig, useForm, useTranslation } from '@payloadcms/ui'
import { Button, SaveButton, Translation, useConfig, useForm, useTranslation } from '@payloadcms/ui'
import React from 'react'
import type {
PluginImportExportTranslationKeys,
PluginImportExportTranslations,
} from '../../translations/index.js'
export const ExportSaveButton: React.FC = () => {
const { t } = useTranslation()
const { t } = useTranslation<PluginImportExportTranslations, PluginImportExportTranslationKeys>()
const {
config: {
routes: { api },
@@ -65,7 +70,7 @@ export const ExportSaveButton: React.FC = () => {
<React.Fragment>
<SaveButton label={label}></SaveButton>
<Button onClick={handleDownload} size="medium" type="button">
Download
<Translation i18nKey="upload:download" t={t} />
</Button>
</React.Fragment>
)

View File

@@ -83,11 +83,12 @@ export const FieldsToExport: SelectFieldClientComponent = (props) => {
return (
<div className={baseClass}>
<FieldLabel label="Columns to Export" />
<FieldLabel label={props.field.label} path={props.path} />
<ReactSelect
className={baseClass}
disabled={props.readOnly}
getOptionValue={(option) => String(option.value)}
inputId={`field-${props.path.replace(/\./g, '__')}`}
isClearable={true}
isMulti={true}
isSortable={true}

View File

@@ -3,13 +3,18 @@ import type { Column } from '@payloadcms/ui'
import type { ClientField, FieldAffectingDataClient } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { Table, useConfig, useField, useTranslation } from '@payloadcms/ui'
import { Table, Translation, useConfig, useField, useTranslation } from '@payloadcms/ui'
import { fieldAffectsData } from 'payload/shared'
import * as qs from 'qs-esm'
import React from 'react'
import { useImportExport } from '../ImportExportProvider/index.js'
import type {
PluginImportExportTranslationKeys,
PluginImportExportTranslations,
} from '../../translations/index.js'
import './index.scss'
import { useImportExport } from '../ImportExportProvider/index.js'
const baseClass = 'preview'
@@ -24,7 +29,10 @@ export const Preview = () => {
const [dataToRender, setDataToRender] = React.useState<any[]>([])
const [resultCount, setResultCount] = React.useState<any>('')
const [columns, setColumns] = React.useState<Column[]>([])
const { i18n } = useTranslation()
const { i18n, t } = useTranslation<
PluginImportExportTranslations,
PluginImportExportTranslationKeys
>()
const collectionSlug = typeof collection === 'string' && collection
const collectionConfig = config.collections.find(
@@ -102,8 +110,20 @@ export const Preview = () => {
return (
<div className={baseClass}>
<div className={`${baseClass}__header`}>
<h3>Preview</h3>
{resultCount && <span>{resultCount} total documents</span>}
<h3>
<Translation i18nKey="version:preview" t={t} />
</h3>
{resultCount && (
<Translation
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
i18nKey="plugin-import-export:totalDocumentsCount"
t={t}
variables={{
count: resultCount,
}}
/>
)}
</div>
{dataToRender && <Table columns={columns} data={dataToRender} />}
</div>

View File

@@ -72,11 +72,12 @@ export const SortBy: SelectFieldClientComponent = (props) => {
return (
<div className={baseClass} style={{ '--field-width': '33%' } as React.CSSProperties}>
<FieldLabel label="Sort By" />
<FieldLabel label={props.field.label} path={props.path} />
<ReactSelect
className={baseClass}
disabled={props.readOnly}
getOptionValue={(option) => String(option.value)}
inputId={`field-${props.path.replace(/\./g, '__')}`}
isClearable={true}
isSortable={true}
// @ts-expect-error react select option

View File

@@ -12,10 +12,11 @@ export const getFields = (config: Config): Field[] => {
width: '33%',
},
defaultValue: 'all',
label: 'Locale',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-locale-label'),
options: [
{
label: 'All Locales',
label: ({ t }) => t('general:allLocales'),
value: 'all',
},
...config.localization.locales.map((locale) => ({
@@ -34,7 +35,8 @@ export const getFields = (config: Config): Field[] => {
name: 'name',
type: 'text',
defaultValue: () => getFilename(),
label: 'File Name',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-name-label'),
},
{
type: 'row',
@@ -46,7 +48,8 @@ export const getFields = (config: Config): Field[] => {
width: '33%',
},
defaultValue: 'csv',
label: 'Export Format',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-format-label'),
options: [
{
label: 'CSV',
@@ -66,6 +69,8 @@ export const getFields = (config: Config): Field[] => {
placeholder: 'No limit',
width: '33%',
},
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-limit-label'),
},
{
name: 'sort',
@@ -75,6 +80,8 @@ export const getFields = (config: Config): Field[] => {
Field: '@payloadcms/plugin-import-export/rsc#SortBy',
},
},
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-sort-label'),
},
],
},
@@ -98,14 +105,15 @@ export const getFields = (config: Config): Field[] => {
width: '33%',
},
defaultValue: 'yes',
label: 'Drafts',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-drafts-label'),
options: [
{
label: 'Yes',
label: ({ t }) => t('general:yes'),
value: 'yes',
},
{
label: 'No',
label: ({ t }) => t('general:no'),
value: 'no',
},
],
@@ -113,6 +121,8 @@ export const getFields = (config: Config): Field[] => {
// {
// name: 'depth',
// type: 'number',
// // @ts-expect-error - this is not correctly typed in plugins right now
// label: ({ t }) => t('plugin-import-export:field-depth-label'),
// admin: {
// width: '33%',
// },
@@ -126,17 +136,22 @@ export const getFields = (config: Config): Field[] => {
name: 'selectionToUse',
type: 'radio',
defaultValue: 'all',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-selectionToUse-label'),
options: [
{
label: 'Use current selection',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:selectionToUse-currentSelection'),
value: 'currentSelection',
},
{
label: 'Use current filters',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:selectionToUse-currentFilters'),
value: 'currentFilters',
},
{
label: 'Use all documents',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:selectionToUse-allDocuments'),
value: 'all',
},
],
@@ -151,6 +166,8 @@ export const getFields = (config: Config): Field[] => {
},
},
hasMany: true,
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-fields-label'),
},
{
name: 'collectionSlug',
@@ -174,7 +191,8 @@ export const getFields = (config: Config): Field[] => {
defaultValue: {},
},
],
label: 'Export Options',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:exportOptions'),
},
{
name: 'preview',

View File

@@ -2,6 +2,7 @@ import type { Config, JobsConfig } from 'payload'
import { deepMergeSimple } from 'payload'
import type { PluginDefaultTranslationsObject } from './translations/types.js'
import type { ImportExportPluginConfig } from './types.js'
import { getCreateCollectionExportTask } from './export/getCreateExportCollectionTask.js'
@@ -70,7 +71,23 @@ export const importExportPlugin =
config.i18n = {}
}
config.i18n.translations = deepMergeSimple(translations, config.i18n?.translations ?? {})
// config.i18n.translations = deepMergeSimple(translations, config.i18n?.translations ?? {})
/**
* Merge plugin translations
*/
const simplifiedTranslations = Object.entries(translations).reduce(
(acc, [key, value]) => {
acc[key] = value.translations
return acc
},
{} as Record<string, PluginDefaultTranslationsObject>,
)
config.i18n = {
...config.i18n,
translations: deepMergeSimple(simplifiedTranslations, config.i18n?.translations ?? {}),
}
return config
}

View File

@@ -1,9 +0,0 @@
import type { GenericTranslationsObject } from '@payloadcms/translations'
export const en: GenericTranslationsObject = {
$schema: './translation-schema.json',
'plugin-seo': {
export: 'Export',
import: 'Import',
},
}

View File

@@ -1,10 +1,90 @@
import type { GenericTranslationsObject, NestedKeysStripped } from '@payloadcms/translations'
import type {
GenericTranslationsObject,
NestedKeysStripped,
SupportedLanguages,
} from '@payloadcms/translations'
import { en } from './en.js'
import type { PluginDefaultTranslationsObject } from './types.js'
import { ar } from './languages/ar.js'
import { az } from './languages/az.js'
import { bg } from './languages/bg.js'
import { ca } from './languages/ca.js'
import { cs } from './languages/cs.js'
import { da } from './languages/da.js'
import { de } from './languages/de.js'
import { en } from './languages/en.js'
import { es } from './languages/es.js'
import { et } from './languages/et.js'
import { fa } from './languages/fa.js'
import { fr } from './languages/fr.js'
import { he } from './languages/he.js'
import { hr } from './languages/hr.js'
import { hu } from './languages/hu.js'
import { hy } from './languages/hy.js'
import { it } from './languages/it.js'
import { ja } from './languages/ja.js'
import { ko } from './languages/ko.js'
import { lt } from './languages/lt.js'
import { my } from './languages/my.js'
import { nb } from './languages/nb.js'
import { nl } from './languages/nl.js'
import { pl } from './languages/pl.js'
import { pt } from './languages/pt.js'
import { ro } from './languages/ro.js'
import { rs } from './languages/rs.js'
import { rsLatin } from './languages/rsLatin.js'
import { ru } from './languages/ru.js'
import { sk } from './languages/sk.js'
import { sl } from './languages/sl.js'
import { sv } from './languages/sv.js'
import { th } from './languages/th.js'
import { tr } from './languages/tr.js'
import { uk } from './languages/uk.js'
import { vi } from './languages/vi.js'
import { zh } from './languages/zh.js'
import { zhTw } from './languages/zhTw.js'
export const translations = {
ar,
az,
bg,
ca,
cs,
da,
de,
en,
}
es,
et,
fa,
fr,
he,
hr,
hu,
hy,
it,
ja,
ko,
lt,
my,
nb,
nl,
pl,
pt,
ro,
rs,
'rs-latin': rsLatin,
ru,
sk,
sl,
sv,
th,
tr,
uk,
vi,
zh,
'zh-TW': zhTw,
} as SupportedLanguages<PluginDefaultTranslationsObject>
export type PluginImportExportTranslations = GenericTranslationsObject

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const arTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'جميع المواقع',
exportDocumentLabel: 'تصدير {{label}}',
exportOptions: 'خيارات التصدير',
'field-depth-label': 'عمق',
'field-drafts-label': 'تضمن المسودات',
'field-fields-label': 'حقول',
'field-format-label': 'تنسيق التصدير',
'field-limit-label': 'حد',
'field-locale-label': 'موقع',
'field-name-label': 'اسم الملف',
'field-selectionToUse-label': 'اختيار للاستخدام',
'field-sort-label': 'ترتيب حسب',
'selectionToUse-allDocuments': 'استخدم جميع الوثائق',
'selectionToUse-currentFilters': 'استخدم الفلاتر الحالية',
'selectionToUse-currentSelection': 'استخدم الاختيار الحالي',
totalDocumentsCount: '{{count}} مستنداً إجمالياً',
},
}
export const ar: PluginLanguage = {
dateFNSKey: 'ar',
translations: arTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const azTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Bütün yerlər',
exportDocumentLabel: '{{label}} ixrac edin',
exportOptions: 'İxrac Variantları',
'field-depth-label': 'Dərinlik',
'field-drafts-label': 'Qaralamaları daxil etin',
'field-fields-label': 'Sahələr',
'field-format-label': 'İxrac Formatı',
'field-limit-label': 'Hədd',
'field-locale-label': 'Yerli',
'field-name-label': 'Fayl adı',
'field-selectionToUse-label': 'İstifadə etmək üçün seçim',
'field-sort-label': 'Sırala',
'selectionToUse-allDocuments': 'Bütün sənədlərdən istifadə edin',
'selectionToUse-currentFilters': 'Cari filtrlərdən istifadə edin',
'selectionToUse-currentSelection': 'Cari seçimi istifadə edin',
totalDocumentsCount: '{{count}} ümumi sənəd',
},
}
export const az: PluginLanguage = {
dateFNSKey: 'az',
translations: azTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const bgTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Всички локации',
exportDocumentLabel: 'Експортиране {{label}}',
exportOptions: 'Опции за експортиране',
'field-depth-label': 'Дълбочина',
'field-drafts-label': 'Включете чернови',
'field-fields-label': 'Полета',
'field-format-label': 'Формат за експортиране',
'field-limit-label': 'Лимит',
'field-locale-label': 'Регион',
'field-name-label': 'Име на файла',
'field-selectionToUse-label': 'Избор за използване',
'field-sort-label': 'Сортирай по',
'selectionToUse-allDocuments': 'Използвайте всички документи',
'selectionToUse-currentFilters': 'Използвайте текущите филтри',
'selectionToUse-currentSelection': 'Използвайте текущия избор',
totalDocumentsCount: '{{count}} общо документа',
},
}
export const bg: PluginLanguage = {
dateFNSKey: 'bg',
translations: bgTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const caTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Totes les localitzacions',
exportDocumentLabel: 'Exporta {{label}}',
exportOptions: "Opcions d'exportació",
'field-depth-label': 'Profunditat',
'field-drafts-label': 'Inclou esborranys',
'field-fields-label': 'Camps',
'field-format-label': "Format d'exportació",
'field-limit-label': 'Límit',
'field-locale-label': 'Local',
'field-name-label': 'Nom del fitxer',
'field-selectionToUse-label': 'Selecció per utilitzar',
'field-sort-label': 'Ordena per',
'selectionToUse-allDocuments': 'Utilitzeu tots els documents',
'selectionToUse-currentFilters': 'Utilitza els filtres actuals',
'selectionToUse-currentSelection': 'Utilitza la selecció actual',
totalDocumentsCount: '{{count}} documents totals',
},
}
export const ca: PluginLanguage = {
dateFNSKey: 'ca',
translations: caTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const csTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Všechny lokalizace',
exportDocumentLabel: 'Export {{label}}',
exportOptions: 'Možnosti exportu',
'field-depth-label': 'Hloubka',
'field-drafts-label': 'Zahrnout návrhy',
'field-fields-label': 'Pole',
'field-format-label': 'Formát exportu',
'field-limit-label': 'Limita',
'field-locale-label': 'Místní',
'field-name-label': 'Název souboru',
'field-selectionToUse-label': 'Výběr k použití',
'field-sort-label': 'Seřadit podle',
'selectionToUse-allDocuments': 'Použijte všechny dokumenty',
'selectionToUse-currentFilters': 'Použijte aktuální filtry',
'selectionToUse-currentSelection': 'Použijte aktuální výběr',
totalDocumentsCount: '{{count}} celkem dokumentů',
},
}
export const cs: PluginLanguage = {
dateFNSKey: 'cs',
translations: csTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const daTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Alle lokaliteter',
exportDocumentLabel: 'Eksport {{label}}',
exportOptions: 'Eksportmuligheder',
'field-depth-label': 'Dybde',
'field-drafts-label': 'Inkluder udkast',
'field-fields-label': 'Felter',
'field-format-label': 'Eksportformat',
'field-limit-label': 'Begrænsning',
'field-locale-label': 'Lokale',
'field-name-label': 'Filnavn',
'field-selectionToUse-label': 'Valg til brug',
'field-sort-label': 'Sorter efter',
'selectionToUse-allDocuments': 'Brug alle dokumenter',
'selectionToUse-currentFilters': 'Brug nuværende filtre',
'selectionToUse-currentSelection': 'Brug nuværende valg',
totalDocumentsCount: '{{count}} samlede dokumenter',
},
}
export const da: PluginLanguage = {
dateFNSKey: 'da',
translations: daTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const deTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Alle Gebietsschemata',
exportDocumentLabel: 'Export {{label}}',
exportOptions: 'Exportoptionen',
'field-depth-label': 'Tiefe',
'field-drafts-label': 'Fügen Sie Entwürfe hinzu',
'field-fields-label': 'Felder',
'field-format-label': 'Exportformat',
'field-limit-label': 'Grenze',
'field-locale-label': 'Ort',
'field-name-label': 'Dateiname',
'field-selectionToUse-label': 'Auswahl zur Verwendung',
'field-sort-label': 'Sortieren nach',
'selectionToUse-allDocuments': 'Verwenden Sie alle Dokumente.',
'selectionToUse-currentFilters': 'Verwenden Sie aktuelle Filter',
'selectionToUse-currentSelection': 'Verwenden Sie die aktuelle Auswahl',
totalDocumentsCount: '{{count}} gesamte Dokumente',
},
}
export const de: PluginLanguage = {
dateFNSKey: 'de',
translations: deTranslations,
}

View File

@@ -0,0 +1,26 @@
import type { PluginLanguage } from '../types.js'
export const enTranslations = {
'plugin-import-export': {
allLocales: 'All locales',
exportDocumentLabel: 'Export {{label}}',
exportOptions: 'Export Options',
'field-depth-label': 'Depth',
'field-drafts-label': 'Include drafts',
'field-fields-label': 'Fields',
'field-format-label': 'Export Format',
'field-limit-label': 'Limit',
'field-locale-label': 'Locale',
'field-name-label': 'File name',
'field-selectionToUse-label': 'Selection to use',
'field-sort-label': 'Sort by',
'selectionToUse-allDocuments': 'Use all documents',
'selectionToUse-currentFilters': 'Use current filters',
'selectionToUse-currentSelection': 'Use current selection',
totalDocumentsCount: '{{count}} total documents',
},
}
export const en: PluginLanguage = {
dateFNSKey: 'en-US',
translations: enTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const esTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Todas las ubicaciones',
exportDocumentLabel: 'Exportar {{label}}',
exportOptions: 'Opciones de Exportación',
'field-depth-label': 'Profundidad',
'field-drafts-label': 'Incluir borradores',
'field-fields-label': 'Campos',
'field-format-label': 'Formato de Exportación',
'field-limit-label': 'Límite',
'field-locale-label': 'Localidad',
'field-name-label': 'Nombre del archivo',
'field-selectionToUse-label': 'Selección para usar',
'field-sort-label': 'Ordenar por',
'selectionToUse-allDocuments': 'Utilice todos los documentos',
'selectionToUse-currentFilters': 'Utilice los filtros actuales',
'selectionToUse-currentSelection': 'Usar selección actual',
totalDocumentsCount: '{{count}} documentos totales',
},
}
export const es: PluginLanguage = {
dateFNSKey: 'es',
translations: esTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const etTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Kõik kohalikud seaded',
exportDocumentLabel: 'Ekspordi {{label}}',
exportOptions: 'Ekspordi valikud',
'field-depth-label': 'Sügavus',
'field-drafts-label': 'Kaasa arvatud mustandid',
'field-fields-label': 'Väljad',
'field-format-label': 'Ekspordi formaat',
'field-limit-label': 'Piirang',
'field-locale-label': 'Lokaal',
'field-name-label': 'Faili nimi',
'field-selectionToUse-label': 'Valiku kasutamine',
'field-sort-label': 'Sorteeri järgi',
'selectionToUse-allDocuments': 'Kasutage kõiki dokumente',
'selectionToUse-currentFilters': 'Kasuta praeguseid filtreid',
'selectionToUse-currentSelection': 'Kasuta praegust valikut',
totalDocumentsCount: '{{count}} dokumendi koguarv',
},
}
export const et: PluginLanguage = {
dateFNSKey: 'et',
translations: etTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const faTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'تمام مکان ها',
exportDocumentLabel: 'صادر کردن {{label}}',
exportOptions: 'گزینه های صادرات',
'field-depth-label': 'عمق',
'field-drafts-label': 'شامل پیش نویس ها',
'field-fields-label': 'مزارع',
'field-format-label': 'فرمت صادرات',
'field-limit-label': 'محدودیت',
'field-locale-label': 'محلی',
'field-name-label': 'نام فایل',
'field-selectionToUse-label': 'انتخاب برای استفاده',
'field-sort-label': 'مرتب سازی بر اساس',
'selectionToUse-allDocuments': 'از تمام مستندات استفاده کنید',
'selectionToUse-currentFilters': 'از فیلترهای فعلی استفاده کنید',
'selectionToUse-currentSelection': 'از انتخاب فعلی استفاده کنید',
totalDocumentsCount: '{{count}} سند کل',
},
}
export const fa: PluginLanguage = {
dateFNSKey: 'fa-IR',
translations: faTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const frTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Tous les paramètres régionaux',
exportDocumentLabel: 'Exporter {{label}}',
exportOptions: "Options d'exportation",
'field-depth-label': 'Profondeur',
'field-drafts-label': 'Inclure les ébauches',
'field-fields-label': 'Champs',
'field-format-label': "Format d'exportation",
'field-limit-label': 'Limite',
'field-locale-label': 'Localisation',
'field-name-label': 'Nom de fichier',
'field-selectionToUse-label': 'Sélection à utiliser',
'field-sort-label': 'Trier par',
'selectionToUse-allDocuments': 'Utilisez tous les documents',
'selectionToUse-currentFilters': 'Utilisez les filtres actuels',
'selectionToUse-currentSelection': 'Utilisez la sélection actuelle',
totalDocumentsCount: '{{count}} documents au total',
},
}
export const fr: PluginLanguage = {
dateFNSKey: 'fr',
translations: frTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const heTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'כל המיקומים',
exportDocumentLabel: 'ייצוא {{label}}',
exportOptions: 'אפשרויות ייצוא',
'field-depth-label': 'עומק',
'field-drafts-label': 'כלול טיוטות',
'field-fields-label': 'שדות',
'field-format-label': 'פורמט יצוא',
'field-limit-label': 'הגבלה',
'field-locale-label': 'מקום',
'field-name-label': 'שם הקובץ',
'field-selectionToUse-label': 'בחירה לשימוש',
'field-sort-label': 'מיין לפי',
'selectionToUse-allDocuments': 'השתמש בכל המסמכים',
'selectionToUse-currentFilters': 'השתמש במסננים הנוכחיים',
'selectionToUse-currentSelection': 'השתמש בבחירה הנוכחית',
totalDocumentsCount: '{{count}} מסמכים כולל',
},
}
export const he: PluginLanguage = {
dateFNSKey: 'he',
translations: heTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const hrTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Sve lokalne postavke',
exportDocumentLabel: 'Izvoz {{label}}',
exportOptions: 'Opcije izvoza',
'field-depth-label': 'Dubina',
'field-drafts-label': 'Uključite nacrte',
'field-fields-label': 'Polja',
'field-format-label': 'Format izvoza',
'field-limit-label': 'Ograničenje',
'field-locale-label': 'Lokalitet',
'field-name-label': 'Naziv datoteke',
'field-selectionToUse-label': 'Odabir za upotrebu',
'field-sort-label': 'Sortiraj po',
'selectionToUse-allDocuments': 'Koristite sve dokumente',
'selectionToUse-currentFilters': 'Koristite trenutne filtre',
'selectionToUse-currentSelection': 'Koristite trenutni odabir',
totalDocumentsCount: '{{count}} ukupno dokumenata',
},
}
export const hr: PluginLanguage = {
dateFNSKey: 'hr',
translations: hrTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const huTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Minden helyszín',
exportDocumentLabel: '{{label}} exportálása',
exportOptions: 'Exportálási lehetőségek',
'field-depth-label': 'Mélység',
'field-drafts-label': 'Tartalmazza a vázlatokat',
'field-fields-label': 'Mezők',
'field-format-label': 'Export formátum',
'field-limit-label': 'Korlát',
'field-locale-label': 'Helyszín',
'field-name-label': 'Fájlnév',
'field-selectionToUse-label': 'Használatra kiválasztva',
'field-sort-label': 'Rendezés szerint',
'selectionToUse-allDocuments': 'Használjon minden dokumentumot',
'selectionToUse-currentFilters': 'Használja az aktuális szűrőket',
'selectionToUse-currentSelection': 'Használja a jelenlegi kiválasztást',
totalDocumentsCount: '{{count}} összes dokumentum',
},
}
export const hu: PluginLanguage = {
dateFNSKey: 'hu',
translations: huTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const hyTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Բոլոր տեղականությունները',
exportDocumentLabel: 'Փոխարտադրել {{label}}',
exportOptions: 'Արտահանման տարբերակներ',
'field-depth-label': 'Խորություն',
'field-drafts-label': 'Ներառեք սևագրեր',
'field-fields-label': 'Դաշտեր',
'field-format-label': 'Արտահանման ձևաչափ',
'field-limit-label': 'Սահմանափակում',
'field-locale-label': 'Լոկալ',
'field-name-label': 'Ֆայլի անվանումը',
'field-selectionToUse-label': 'Օգտագործման ընտրություն',
'field-sort-label': 'Դասավորել ըստ',
'selectionToUse-allDocuments': 'Օգտագործեք բոլոր փաստաթղթերը',
'selectionToUse-currentFilters': 'Օգտագործեք ընթացիկ ֆիլտրերը',
'selectionToUse-currentSelection': 'Օգտագործել ընթացիկ ընտրությունը',
totalDocumentsCount: '{{count}} ընդհանուր փաստաթուղթեր',
},
}
export const hy: PluginLanguage = {
dateFNSKey: 'hy-AM',
translations: hyTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const itTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Tutte le località',
exportDocumentLabel: 'Esporta {{label}}',
exportOptions: 'Opzioni di Esportazione',
'field-depth-label': 'Profondità',
'field-drafts-label': 'Includi bozze',
'field-fields-label': 'Campi',
'field-format-label': 'Formato di Esportazione',
'field-limit-label': 'Limite',
'field-locale-label': 'Locale',
'field-name-label': 'Nome del file',
'field-selectionToUse-label': 'Selezione da utilizzare',
'field-sort-label': 'Ordina per',
'selectionToUse-allDocuments': 'Utilizza tutti i documenti',
'selectionToUse-currentFilters': 'Utilizza i filtri correnti',
'selectionToUse-currentSelection': 'Utilizza la selezione corrente',
totalDocumentsCount: '{{count}} documenti totali',
},
}
export const it: PluginLanguage = {
dateFNSKey: 'it',
translations: itTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const jaTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'すべてのロケール',
exportDocumentLabel: '{{label}}をエクスポートする',
exportOptions: 'エクスポートオプション',
'field-depth-label': '深さ',
'field-drafts-label': 'ドラフトを含めます',
'field-fields-label': 'フィールド',
'field-format-label': 'エクスポート形式',
'field-limit-label': '制限',
'field-locale-label': 'ロケール',
'field-name-label': 'ファイル名',
'field-selectionToUse-label': '使用する選択',
'field-sort-label': '並び替える',
'selectionToUse-allDocuments': 'すべての文書を使用してください。',
'selectionToUse-currentFilters': '現在のフィルターを使用してください',
'selectionToUse-currentSelection': '現在の選択を使用する',
totalDocumentsCount: '{{count}}合計の文書',
},
}
export const ja: PluginLanguage = {
dateFNSKey: 'ja',
translations: jaTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const koTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: '모든 지역 설정',
exportDocumentLabel: '{{label}} 내보내기',
exportOptions: '수출 옵션',
'field-depth-label': '깊이',
'field-drafts-label': '초안을 포함하십시오.',
'field-fields-label': '필드',
'field-format-label': '수출 형식',
'field-limit-label': '한계',
'field-locale-label': '지역',
'field-name-label': '파일 이름',
'field-selectionToUse-label': '사용할 선택',
'field-sort-label': '정렬 방식',
'selectionToUse-allDocuments': '모든 문서를 사용하십시오.',
'selectionToUse-currentFilters': '현재 필터를 사용하십시오.',
'selectionToUse-currentSelection': '현재 선택 항목을 사용하십시오.',
totalDocumentsCount: '{{count}}개의 총 문서',
},
}
export const ko: PluginLanguage = {
dateFNSKey: 'ko',
translations: koTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ltTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Visos vietovės',
exportDocumentLabel: 'Eksportuoti {{label}}',
exportOptions: 'Eksporto parinktys',
'field-depth-label': 'Gylis',
'field-drafts-label': 'Įtraukite juodraščius',
'field-fields-label': 'Laukai',
'field-format-label': 'Eksporto formatas',
'field-limit-label': 'Ribos',
'field-locale-label': 'Lokalė',
'field-name-label': 'Failo pavadinimas',
'field-selectionToUse-label': 'Naudojimo pasirinkimas',
'field-sort-label': 'Rūšiuoti pagal',
'selectionToUse-allDocuments': 'Naudokite visus dokumentus.',
'selectionToUse-currentFilters': 'Naudoti esamus filtrus',
'selectionToUse-currentSelection': 'Naudoti dabartinį pasirinkimą',
totalDocumentsCount: '{{count}} viso dokumentų',
},
}
export const lt: PluginLanguage = {
dateFNSKey: 'lt',
translations: ltTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const lvTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Visas lokalitātes',
exportDocumentLabel: 'Eksportēt {{label}}',
exportOptions: 'Eksportēšanas opcijas',
'field-depth-label': 'Dziļums',
'field-drafts-label': 'Iekļaut melnrakstus',
'field-fields-label': 'Lauki',
'field-format-label': 'Eksporta formāts',
'field-limit-label': 'Limits',
'field-locale-label': 'Lokalizācija',
'field-name-label': 'Faila nosaukums',
'field-selectionToUse-label': 'Izvēles lietošana',
'field-sort-label': 'Kārtot pēc',
'selectionToUse-allDocuments': 'Izmantojiet visus dokumentus',
'selectionToUse-currentFilters': 'Izmantot pašreizējos filtrus',
'selectionToUse-currentSelection': 'Izmantot pašreizējo izvēli',
totalDocumentsCount: '{{count}} kopā dokumenti',
},
}
export const lv: PluginLanguage = {
dateFNSKey: 'lv',
translations: lvTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const myTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'အားလုံးနေရာတွင်',
exportDocumentLabel: 'Eksport {{label}}',
exportOptions: 'Pilihan Eksport',
'field-depth-label': 'အန္တိုင်း',
'field-drafts-label': 'မူကြမ်းများပါဝင်ပါ',
'field-fields-label': 'ကွင်းပျိုးရန်ကွက်များ',
'field-format-label': 'တင်ပို့နည်းအစီအစဉ်',
'field-limit-label': 'ကန့်သတ်ချက်',
'field-locale-label': 'Tempatan',
'field-name-label': 'ဖိုင်နာမည်',
'field-selectionToUse-label': 'Pilihan untuk digunakan',
'field-sort-label': 'စီမံအလိုက်',
'selectionToUse-allDocuments': 'Gunakan semua dokumen',
'selectionToUse-currentFilters': 'Gunakan penapis semasa',
'selectionToUse-currentSelection': 'Gunakan pilihan semasa',
totalDocumentsCount: '{{count}} keseluruhan dokumen',
},
}
export const my: PluginLanguage = {
dateFNSKey: 'en-US',
translations: myTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const nbTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Alle steder',
exportDocumentLabel: 'Eksporter {{label}}',
exportOptions: 'Eksportalternativer',
'field-depth-label': 'Dybde',
'field-drafts-label': 'Inkluder utkast',
'field-fields-label': 'Felt',
'field-format-label': 'Eksportformat',
'field-limit-label': 'Begrensning',
'field-locale-label': 'Lokal',
'field-name-label': 'Filnavn',
'field-selectionToUse-label': 'Valg til bruk',
'field-sort-label': 'Sorter etter',
'selectionToUse-allDocuments': 'Bruk alle dokumentene',
'selectionToUse-currentFilters': 'Bruk gjeldende filtre',
'selectionToUse-currentSelection': 'Bruk gjeldende utvalg',
totalDocumentsCount: '{{count}} totalt dokumenter',
},
}
export const nb: PluginLanguage = {
dateFNSKey: 'nb',
translations: nbTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const nlTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Alle locaties',
exportDocumentLabel: 'Exporteer {{label}}',
exportOptions: 'Exportmogelijkheden',
'field-depth-label': 'Diepte',
'field-drafts-label': 'Voeg ontwerpen toe',
'field-fields-label': 'Velden',
'field-format-label': 'Exportformaat',
'field-limit-label': 'Limiet',
'field-locale-label': 'Lokale',
'field-name-label': 'Bestandsnaam',
'field-selectionToUse-label': 'Selectie om te gebruiken',
'field-sort-label': 'Sorteer op',
'selectionToUse-allDocuments': 'Gebruik alle documenten',
'selectionToUse-currentFilters': 'Gebruik huidige filters',
'selectionToUse-currentSelection': 'Gebruik huidige selectie',
totalDocumentsCount: '{{count}} totaal aantal documenten',
},
}
export const nl: PluginLanguage = {
dateFNSKey: 'nl',
translations: nlTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const plTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Wszystkie lokalizacje',
exportDocumentLabel: 'Eksportuj {{label}}',
exportOptions: 'Opcje eksportu',
'field-depth-label': 'Głębokość',
'field-drafts-label': 'Dołącz szkice',
'field-fields-label': 'Pola',
'field-format-label': 'Format eksportu',
'field-limit-label': 'Limit',
'field-locale-label': 'Lokalizacja',
'field-name-label': 'Nazwa pliku',
'field-selectionToUse-label': 'Wybór do użycia',
'field-sort-label': 'Sortuj według',
'selectionToUse-allDocuments': 'Użyj wszystkich dokumentów.',
'selectionToUse-currentFilters': 'Użyj aktualnych filtrów',
'selectionToUse-currentSelection': 'Użyj aktualnego wyboru',
totalDocumentsCount: '{{count}} łączna liczba dokumentów',
},
}
export const pl: PluginLanguage = {
dateFNSKey: 'pl',
translations: plTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ptTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Todos os locais',
exportDocumentLabel: 'Exportar {{label}}',
exportOptions: 'Opções de Exportação',
'field-depth-label': 'Profundidade',
'field-drafts-label': 'Incluir rascunhos',
'field-fields-label': 'Campos',
'field-format-label': 'Formato de Exportação',
'field-limit-label': 'Limite',
'field-locale-label': 'Localização',
'field-name-label': 'Nome do arquivo',
'field-selectionToUse-label': 'Seleção para usar',
'field-sort-label': 'Ordenar por',
'selectionToUse-allDocuments': 'Use todos os documentos',
'selectionToUse-currentFilters': 'Use os filtros atuais',
'selectionToUse-currentSelection': 'Use a seleção atual',
totalDocumentsCount: '{{count}} documentos totais',
},
}
export const pt: PluginLanguage = {
dateFNSKey: 'pt',
translations: ptTranslations,
}

View File

@@ -0,0 +1,3 @@
for file in *.js; do
mv -- "$file" "${file%.js}.ts"
done

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const roTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Toate locațiile',
exportDocumentLabel: 'Export {{label}}',
exportOptions: 'Opțiuni de export',
'field-depth-label': 'Adâncime',
'field-drafts-label': 'Includează schițe',
'field-fields-label': 'Campuri',
'field-format-label': 'Format de export',
'field-limit-label': 'Limită',
'field-locale-label': 'Localizare',
'field-name-label': 'Numele fișierului',
'field-selectionToUse-label': 'Selectarea pentru utilizare',
'field-sort-label': 'Sortează după',
'selectionToUse-allDocuments': 'Utilizați toate documentele.',
'selectionToUse-currentFilters': 'Utilizați filtrele curente',
'selectionToUse-currentSelection': 'Utilizați selecția curentă',
totalDocumentsCount: '{{count}} documente totale',
},
}
export const ro: PluginLanguage = {
dateFNSKey: 'ro',
translations: roTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const rsTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Sve lokacije',
exportDocumentLabel: 'Извоз {{label}}',
exportOptions: 'Опције извоза',
'field-depth-label': 'Dubina',
'field-drafts-label': 'Uključite nacrte',
'field-fields-label': 'Polja',
'field-format-label': 'Format izvoza',
'field-limit-label': 'Ograničenje',
'field-locale-label': 'Локалитет',
'field-name-label': 'Ime datoteke',
'field-selectionToUse-label': 'Izbor za upotrebu',
'field-sort-label': 'Sortiraj po',
'selectionToUse-allDocuments': 'Koristite sve dokumente',
'selectionToUse-currentFilters': 'Koristite trenutne filtere',
'selectionToUse-currentSelection': 'Koristite trenutni izbor',
totalDocumentsCount: '{{count}} ukupno dokumenata',
},
}
export const rs: PluginLanguage = {
dateFNSKey: 'rs',
translations: rsTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const rsLatinTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Sve lokalne postavke',
exportDocumentLabel: 'Izvoz {{label}}',
exportOptions: 'Opcije izvoza',
'field-depth-label': 'Dubina',
'field-drafts-label': 'Uključite nacrte',
'field-fields-label': 'Polja',
'field-format-label': 'Format izvoza',
'field-limit-label': 'Ograničenje',
'field-locale-label': 'Lokalitet',
'field-name-label': 'Ime datoteke',
'field-selectionToUse-label': 'Izbor za upotrebu',
'field-sort-label': 'Sortiraj po',
'selectionToUse-allDocuments': 'Koristite sve dokumente',
'selectionToUse-currentFilters': 'Koristite trenutne filtere',
'selectionToUse-currentSelection': 'Koristi trenutni izbor',
totalDocumentsCount: '{{count}} ukupno dokumenata',
},
}
export const rsLatin: PluginLanguage = {
dateFNSKey: 'rs-Latin',
translations: rsLatinTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ruTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Все локали',
exportDocumentLabel: 'Экспорт {{label}}',
exportOptions: 'Опции экспорта',
'field-depth-label': 'Глубина',
'field-drafts-label': 'Включить черновики',
'field-fields-label': 'Поля',
'field-format-label': 'Формат экспорта',
'field-limit-label': 'Лимит',
'field-locale-label': 'Локаль',
'field-name-label': 'Имя файла',
'field-selectionToUse-label': 'Выбор использования',
'field-sort-label': 'Сортировать по',
'selectionToUse-allDocuments': 'Используйте все документы',
'selectionToUse-currentFilters': 'Использовать текущие фильтры',
'selectionToUse-currentSelection': 'Использовать текущий выбор',
totalDocumentsCount: '{{count}} общее количество документов',
},
}
export const ru: PluginLanguage = {
dateFNSKey: 'ru',
translations: ruTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const skTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Všetky miestne nastavenia',
exportDocumentLabel: 'Export {{label}}',
exportOptions: 'Možnosti exportu',
'field-depth-label': 'Hĺbka',
'field-drafts-label': 'Zahrnúť návrhy',
'field-fields-label': 'Polia',
'field-format-label': 'Formát exportu',
'field-limit-label': 'Limit',
'field-locale-label': 'Lokalita',
'field-name-label': 'Názov súboru',
'field-selectionToUse-label': 'Výber na použitie',
'field-sort-label': 'Triediť podľa',
'selectionToUse-allDocuments': 'Použite všetky dokumenty',
'selectionToUse-currentFilters': 'Použiť aktuálne filtre',
'selectionToUse-currentSelection': 'Použiť aktuálny výber',
totalDocumentsCount: '{{count}} celkový počet dokumentov',
},
}
export const sk: PluginLanguage = {
dateFNSKey: 'sk',
translations: skTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const slTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Vse lokacije',
exportDocumentLabel: 'Izvozi {{label}}',
exportOptions: 'Možnosti izvoza',
'field-depth-label': 'Globina',
'field-drafts-label': 'Vključi osnutke',
'field-fields-label': 'Polja',
'field-format-label': 'Format izvoza',
'field-limit-label': 'Omejitev',
'field-locale-label': 'Lokalno',
'field-name-label': 'Ime datoteke',
'field-selectionToUse-label': 'Izbor za uporabo',
'field-sort-label': 'Razvrsti po',
'selectionToUse-allDocuments': 'Uporabite vse dokumente',
'selectionToUse-currentFilters': 'Uporabite trenutne filtre.',
'selectionToUse-currentSelection': 'Uporabi trenutno izbiro',
totalDocumentsCount: '{{count}} skupno dokumentov',
},
}
export const sl: PluginLanguage = {
dateFNSKey: 'sl-SI',
translations: slTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const svTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Alla platser',
exportDocumentLabel: 'Exportera {{label}}',
exportOptions: 'Exportalternativ',
'field-depth-label': 'Djup',
'field-drafts-label': 'Inkludera utkast',
'field-fields-label': 'Fält',
'field-format-label': 'Exportformat',
'field-limit-label': 'Begränsning',
'field-locale-label': 'Lokal',
'field-name-label': 'Filnamn',
'field-selectionToUse-label': 'Val att använda',
'field-sort-label': 'Sortera efter',
'selectionToUse-allDocuments': 'Använd alla dokument',
'selectionToUse-currentFilters': 'Använd aktuella filter',
'selectionToUse-currentSelection': 'Använd nuvarande urval',
totalDocumentsCount: '{{count}} totala dokument',
},
}
export const sv: PluginLanguage = {
dateFNSKey: 'sv',
translations: svTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const thTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'ทุกสถานที่',
exportDocumentLabel: 'ส่งออก {{label}}',
exportOptions: 'ตัวเลือกการส่งออก',
'field-depth-label': 'ความลึก',
'field-drafts-label': 'รวมฉบับร่าง',
'field-fields-label': 'สนาม',
'field-format-label': 'รูปแบบการส่งออก',
'field-limit-label': 'จำกัด',
'field-locale-label': 'ที่ตั้ง',
'field-name-label': 'ชื่อไฟล์',
'field-selectionToUse-label': 'การเลือกใช้',
'field-sort-label': 'เรียงตาม',
'selectionToUse-allDocuments': 'ใช้เอกสารทั้งหมด',
'selectionToUse-currentFilters': 'ใช้ตัวกรองปัจจุบัน',
'selectionToUse-currentSelection': 'ใช้การเลือกปัจจุบัน',
totalDocumentsCount: '{{count}} เอกสารทั้งหมด',
},
}
export const th: PluginLanguage = {
dateFNSKey: 'th',
translations: thTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const trTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Tüm yerler',
exportDocumentLabel: '{{label}} dışa aktar',
exportOptions: 'İhracat Seçenekleri',
'field-depth-label': 'Derinlik',
'field-drafts-label': 'Taslakları dahil et',
'field-fields-label': 'Alanlar',
'field-format-label': 'İhracat Formatı',
'field-limit-label': 'Sınır',
'field-locale-label': 'Yerel',
'field-name-label': 'Dosya adı',
'field-selectionToUse-label': 'Kullanılacak seçim',
'field-sort-label': 'Sırala',
'selectionToUse-allDocuments': 'Tüm belgeleri kullanın',
'selectionToUse-currentFilters': 'Mevcut filtreleri kullanın',
'selectionToUse-currentSelection': 'Mevcut seçimi kullanın',
totalDocumentsCount: '{{count}} toplam belge',
},
}
export const tr: PluginLanguage = {
dateFNSKey: 'tr',
translations: trTranslations,
}

View File

@@ -0,0 +1,107 @@
{
"type": "object",
"$schema": "http://json-schema.org/draft-04/schema#",
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string"
},
"plugin-import-export": {
"type": "object",
"additionalProperties": false,
"properties": {
"export": {
"type": "string"
},
"import": {
"type": "string"
},
"allLocales": {
"type": "string"
},
"download": {
"type": "string"
},
"exportDocumentLabel": {
"type": "string"
},
"exportOptions": {
"type": "string"
},
"field-depth-label": {
"type": "string"
},
"field-drafts-label": {
"type": "string"
},
"field-fields-label": {
"type": "string"
},
"field-format-label": {
"type": "string"
},
"field-limit-label": {
"type": "string"
},
"field-locale-label": {
"type": "string"
},
"field-name-label": {
"type": "string"
},
"field-selectionToUse-label": {
"type": "string"
},
"field-sort-label": {
"type": "string"
},
"no": {
"type": "string"
},
"preview": {
"type": "string"
},
"selectionToUse-allDocuments": {
"type": "string"
},
"selectionToUse-currentFilters": {
"type": "string"
},
"selectionToUse-currentSelection": {
"type": "string"
},
"totalDocumentsCount": {
"type": "string"
},
"yes": {
"type": "string"
}
},
"required": [
"export",
"import",
"allLocales",
"download",
"exportDocumentLabel",
"exportOptions",
"field-depth-label",
"field-drafts-label",
"field-fields-label",
"field-format-label",
"field-limit-label",
"field-locale-label",
"field-name-label",
"field-selectionToUse-label",
"field-sort-label",
"no",
"preview",
"selectionToUse-allDocuments",
"selectionToUse-currentFilters",
"selectionToUse-currentSelection",
"totalDocumentsCount",
"yes"
]
}
},
"required": ["plugin-import-export"]
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ukTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Всі локалі',
exportDocumentLabel: 'Експорт {{label}}',
exportOptions: 'Опції експорту',
'field-depth-label': 'Глибина',
'field-drafts-label': 'Включити чернетки',
'field-fields-label': 'Поля',
'field-format-label': 'Формат експорту',
'field-limit-label': 'Обмеження',
'field-locale-label': 'Локалізація',
'field-name-label': 'Назва файлу',
'field-selectionToUse-label': 'Вибір для використання',
'field-sort-label': 'Сортувати за',
'selectionToUse-allDocuments': 'Використовуйте всі документи',
'selectionToUse-currentFilters': 'Використовувати поточні фільтри',
'selectionToUse-currentSelection': 'Використовуйте поточний вибір',
totalDocumentsCount: '{{count}} всього документів',
},
}
export const uk: PluginLanguage = {
dateFNSKey: 'uk',
translations: ukTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const viTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: 'Tất cả địa điểm',
exportDocumentLabel: 'Xuất {{label}}',
exportOptions: 'Tùy chọn xuất',
'field-depth-label': 'Độ sâu',
'field-drafts-label': 'Bao gồm bản thảo',
'field-fields-label': 'Cánh đồng',
'field-format-label': 'Định dạng Xuất khẩu',
'field-limit-label': 'Giới hạn',
'field-locale-label': 'Địa phương',
'field-name-label': 'Tên tệp',
'field-selectionToUse-label': 'Lựa chọn để sử dụng',
'field-sort-label': 'Sắp xếp theo',
'selectionToUse-allDocuments': 'Sử dụng tất cả các tài liệu',
'selectionToUse-currentFilters': 'Sử dụng bộ lọc hiện tại',
'selectionToUse-currentSelection': 'Sử dụng lựa chọn hiện tại',
totalDocumentsCount: '{{count}} tổng số tài liệu',
},
}
export const vi: PluginLanguage = {
dateFNSKey: 'vi',
translations: viTranslations,
}

View File

@@ -0,0 +1,27 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const zhTranslations: PluginDefaultTranslationsObject = {
'plugin-import-export': {
allLocales: '所有地方',
exportDocumentLabel: '导出{{label}}',
exportOptions: '导出选项',
'field-depth-label': '深度',
'field-drafts-label': '包括草稿',
'field-fields-label': '领域',
'field-format-label': '导出格式',
'field-limit-label': '限制',
'field-locale-label': '地区设置',
'field-name-label': '文件名',
'field-selectionToUse-label': '使用选择',
'field-sort-label': '按排序',
'selectionToUse-allDocuments': '使用所有文档',
'selectionToUse-currentFilters': '使用当前过滤器',
'selectionToUse-currentSelection': '使用当前选择',
totalDocumentsCount: '{{count}}份总文件',
},
}
export const zh: PluginLanguage = {
dateFNSKey: 'zh-CN',
translations: zhTranslations,
}

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