chore: update 2.0 branch from master (#3207)

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
Co-authored-by: PatrikKozak <patrik@trbl.design>
Co-authored-by: Lucas Blancas <lablancas@gmail.com>
Co-authored-by: Stef Gootzen <37367280+stefgootzen@users.noreply.github.com>
Co-authored-by: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com>
Co-authored-by: Jessica Chowdhury <67977755+JessChowdhury@users.noreply.github.com>
Co-authored-by: PatrikKozak <35232443+PatrikKozak@users.noreply.github.com>
Co-authored-by: Greg Willard <Wickett06@gmail.com>
Co-authored-by: James Mikrut <james@payloadcms.com>
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
fix: WhereBuilder component does not accept all valid Where queries (#3087)
fix: passes in height to resizeOptions upload option to allow height resize (#3171)
This commit is contained in:
Alessio Gravili
2023-08-22 22:04:50 +02:00
committed by GitHub
parent f911257cd9
commit 9467074fb9
174 changed files with 3875 additions and 2791 deletions

View File

@@ -1,5 +1,27 @@
# [1.14.0](https://github.com/payloadcms/payload/compare/v1.13.4...v1.14.0) (2023-08-16)
### Bug Fixes
* DatePicker showing only selected day by default ([#3169](https://github.com/payloadcms/payload/issues/3169)) ([edcb393](https://github.com/payloadcms/payload/commit/edcb3933cfb4532180c822135ea6a8be928e0fdc))
* only allow redirects to /admin sub-routes ([c0f05a1](https://github.com/payloadcms/payload/commit/c0f05a1c38fb9c958de920fabb698b5ecfb661f0))
* passes in height to resizeOptions upload option to allow height resize ([#3171](https://github.com/payloadcms/payload/issues/3171)) ([7963d04](https://github.com/payloadcms/payload/commit/7963d04a27888eb5a12d0ab37f2082cd33638abd))
* WhereBuilder component does not accept all valid Where queries ([#3087](https://github.com/payloadcms/payload/issues/3087)) ([fdfdfc8](https://github.com/payloadcms/payload/commit/fdfdfc83f36a958971f8e4e4f9f5e51560cb26e0))
### Features
* add afterOperation hook ([#2697](https://github.com/payloadcms/payload/issues/2697)) ([33686c6](https://github.com/payloadcms/payload/commit/33686c6db8373a16d7f6b0192e0701bf15881aa4))
* add support for hotkeys ([#1821](https://github.com/payloadcms/payload/issues/1821)) ([942cfec](https://github.com/payloadcms/payload/commit/942cfec286ff050e13417b037cca64b9d757d868))
* Added Azerbaijani language file ([#3164](https://github.com/payloadcms/payload/issues/3164)) ([63e3063](https://github.com/payloadcms/payload/commit/63e3063b9ecc1afd62d7a287a798d41215008f2a))
* allow async relationship filter options ([#2951](https://github.com/payloadcms/payload/issues/2951)) ([bad3638](https://github.com/payloadcms/payload/commit/bad363882c9d00d3c73547ca3329eba988e728ff))
* Improve admin dashboard accessibility ([#3053](https://github.com/payloadcms/payload/issues/3053)) ([e03a8e6](https://github.com/payloadcms/payload/commit/e03a8e6b030e82a17e1cdae5b4032433cf9c75a4))
* improve field ops ([#3172](https://github.com/payloadcms/payload/issues/3172)) ([d91b44c](https://github.com/payloadcms/payload/commit/d91b44cbb3fd526caca2a6f4bd30fd06ede3a5da))
* make PAYLOAD_CONFIG_PATH optional ([#2839](https://github.com/payloadcms/payload/issues/2839)) ([5744de7](https://github.com/payloadcms/payload/commit/5744de7ec63e3f17df7e02a7cc827818a79dbbb8))
* text alignment for richtext editor ([#2803](https://github.com/payloadcms/payload/issues/2803)) ([a0b13a5](https://github.com/payloadcms/payload/commit/a0b13a5b01fa0d7f4c4dffd1895bfe507e5c676d))
## [1.13.4](https://github.com/payloadcms/payload/compare/v1.13.3...v1.13.4) (2023-08-11) ## [1.13.4](https://github.com/payloadcms/payload/compare/v1.13.3...v1.13.4) (2023-08-11)

View File

@@ -20,6 +20,12 @@ Payload documentation can be found directly within its codebase and you can feel
If you're an incredibly awesome person and want to help us make Payload even better through new features or additions, we would be thrilled to work with you. If you're an incredibly awesome person and want to help us make Payload even better through new features or additions, we would be thrilled to work with you.
## Design Contributions
When it comes to design-related changes or additions, it's crucial for us to ensure a cohesive user experience and alignment with our broader design vision. Before embarking on any implementation that would affect the design or UI/UX, we ask that you **first share your design proposal** with us for review and approval.
Our design review ensures that proposed changes fit seamlessly with other components, both existing and planned. This step is meant to prevent unintentional design inconsistencies and to save you from investing time in implementing features that might need significant design alterations later.
### Before Starting ### Before Starting
To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request. To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request.

View File

@@ -7,9 +7,7 @@ keywords: rich text, fields, config, configuration, documentation, Content Manag
--- ---
<Banner> <Banner>
The Rich Text field is a powerful way to allow editors to write dynamic The Rich Text field is a powerful way to allow editors to write dynamic content. The content is saved as JSON in the database and can be converted into any format, including HTML, that you need.
content. The content is saved as JSON in the database and can be converted
into any format, including HTML, that you need.
</Banner> </Banner>
<LightDarkImage <LightDarkImage
@@ -22,14 +20,7 @@ keywords: rich text, fields, config, configuration, documentation, Content Manag
The Admin component is built on the powerful [`slatejs`](https://docs.slatejs.org/) editor and is meant to be as extensible and customizable as possible. The Admin component is built on the powerful [`slatejs`](https://docs.slatejs.org/) editor and is meant to be as extensible and customizable as possible.
<Banner type="success"> <Banner type="success">
<strong> <strong>Consistent with Payload's goal of making you learn as little of Payload as possible, customizing and using the Rich Text Editor does not involve learning how to develop for a <em>Payload</em> rich text editor.</strong> Instead, you can invest your time and effort into learning Slate, an open-source tool that will allow you to apply your learnings elsewhere as well.
Consistent with Payload's goal of making you learn as little of Payload as
possible, customizing and using the Rich Text Editor does not involve
learning how to develop for a <em>Payload</em> rich text editor.
</strong>{" "}
Instead, you can invest your time and effort into learning Slate, an
open-source tool that will allow you to apply your learnings elsewhere as
well.
</Banner> </Banner>
### Config ### Config
@@ -129,13 +120,7 @@ The built-in `relationship` element is a powerful way to reference other Documen
Similar to the `relationship` element, the `upload` element is a user-friendly way to reference [Upload-enabled collections](/docs/upload/overview) with a UI specifically designed for media / image-based uploads. Similar to the `relationship` element, the `upload` element is a user-friendly way to reference [Upload-enabled collections](/docs/upload/overview) with a UI specifically designed for media / image-based uploads.
<Banner type="success"> <Banner type="success">
<strong>Tip:</strong> <strong>Tip:</strong><br />Collections are automatically allowed to be selected within the Rich Text relationship and upload elements by default. If you want to disable a collection from being able to be referenced in Rich Text fields, set the collection admin options of <strong>enableRichTextLink</strong> and <strong>enableRichTextRelationship</strong> to false.
<br />
Collections are automatically allowed to be selected within the Rich Text
relationship and upload elements by default. If you want to disable a
collection from being able to be referenced in Rich Text fields, set the
collection admin options of <strong>enableRichTextLink</strong> and{" "}
<strong>enableRichTextRelationship</strong> to false.
</Banner> </Banner>
Relationship and Upload elements are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` or `upload` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize. Relationship and Upload elements are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` or `upload` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize.
@@ -311,10 +296,7 @@ const serialize = (children) =>
``` ```
<Banner> <Banner>
<strong>Note:</strong> <strong>Note:</strong><br />The above example is for how to render to JSX, although for plain HTML the pattern is similar. Just remove the JSX and return HTML strings instead!
<br />
The above example is for how to render to JSX, although for plain HTML the
pattern is similar. Just remove the JSX and return HTML strings instead!
</Banner> </Banner>
### Built-in SlateJS Plugins ### Built-in SlateJS Plugins

View File

@@ -16,6 +16,7 @@ Collections feature the ability to define the following hooks:
- [afterRead](#afterread) - [afterRead](#afterread)
- [beforeDelete](#beforedelete) - [beforeDelete](#beforedelete)
- [afterDelete](#afterdelete) - [afterDelete](#afterdelete)
- [afterOperation](#afteroperation)
Additionally, `auth`-enabled collections feature the following hooks: Additionally, `auth`-enabled collections feature the following hooks:
@@ -31,6 +32,7 @@ Additionally, `auth`-enabled collections feature the following hooks:
All collection Hook properties accept arrays of synchronous or asynchronous functions. Each Hook type receives specific arguments and has the ability to modify specific outputs. All collection Hook properties accept arrays of synchronous or asynchronous functions. Each Hook type receives specific arguments and has the ability to modify specific outputs.
`collections/exampleHooks.js` `collections/exampleHooks.js`
```ts ```ts
import { CollectionConfig } from 'payload/types'; import { CollectionConfig } from 'payload/types';
@@ -48,6 +50,7 @@ export const ExampleHooks: CollectionConfig = {
afterChange: [(args) => {...}], afterChange: [(args) => {...}],
afterRead: [(args) => {...}], afterRead: [(args) => {...}],
afterDelete: [(args) => {...}], afterDelete: [(args) => {...}],
afterOperation: [(args) => {...}],
// Auth-enabled hooks // Auth-enabled hooks
beforeLogin: [(args) => {...}], beforeLogin: [(args) => {...}],
@@ -62,19 +65,19 @@ export const ExampleHooks: CollectionConfig = {
### beforeOperation ### beforeOperation
The `beforeOperation` Hook type can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins. The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh` and `forgotPassword`. Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh`, and `forgotPassword`.
```ts ```ts
import { CollectionBeforeOperationHook } from 'payload/types'; import { CollectionBeforeOperationHook } from "payload/types";
const beforeOperationHook: CollectionBeforeOperationHook = async ({ const beforeOperationHook: CollectionBeforeOperationHook = async ({
args, // Original arguments passed into the operation args, // original arguments passed into the operation
operation, // name of the operation operation, // name of the operation
}) => { }) => {
return args; // Return operation arguments as necessary return args; // return modified operation arguments as necessary
} };
``` ```
### beforeValidate ### beforeValidate
@@ -88,7 +91,7 @@ Please do note that this does not run before the client-side validation. If you
3. `validate` runs on the server 3. `validate` runs on the server
```ts ```ts
import { CollectionBeforeOperationHook } from 'payload/types'; import { CollectionBeforeOperationHook } from "payload/types";
const beforeValidateHook: CollectionBeforeValidateHook = async ({ const beforeValidateHook: CollectionBeforeValidateHook = async ({
data, // incoming data to update or create with data, // incoming data to update or create with
@@ -97,7 +100,7 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({
originalDoc, // original document originalDoc, // original document
}) => { }) => {
return data; // Return data to either create or update a document with return data; // Return data to either create or update a document with
} };
``` ```
### beforeChange ### beforeChange
@@ -105,7 +108,7 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved. Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved.
```ts ```ts
import { CollectionBeforeChangeHook } from 'payload/types'; import { CollectionBeforeChangeHook } from "payload/types";
const beforeChangeHook: CollectionBeforeChangeHook = async ({ const beforeChangeHook: CollectionBeforeChangeHook = async ({
data, // incoming data to update or create with data, // incoming data to update or create with
@@ -114,7 +117,7 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({
originalDoc, // original document originalDoc, // original document
}) => { }) => {
return data; // Return data to either create or update a document with return data; // Return data to either create or update a document with
} };
``` ```
### afterChange ### afterChange
@@ -122,7 +125,7 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({
After a document is created or updated, the `afterChange` hook runs. This hook is helpful to recalculate statistics such as total sales within a global, syncing user profile changes to a CRM, and more. After a document is created or updated, the `afterChange` hook runs. This hook is helpful to recalculate statistics such as total sales within a global, syncing user profile changes to a CRM, and more.
```ts ```ts
import { CollectionAfterChangeHook } from 'payload/types'; import { CollectionAfterChangeHook } from "payload/types";
const afterChangeHook: CollectionAfterChangeHook = async ({ const afterChangeHook: CollectionAfterChangeHook = async ({
doc, // full document data doc, // full document data
@@ -131,7 +134,7 @@ const afterChangeHook: CollectionAfterChangeHook = async ({
operation, // name of the operation ie. 'create', 'update' operation, // name of the operation ie. 'create', 'update'
}) => { }) => {
return doc; return doc;
} };
``` ```
### beforeRead ### beforeRead
@@ -139,7 +142,7 @@ const afterChangeHook: CollectionAfterChangeHook = async ({
Runs before `find` and `findByID` operations are transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument. Runs before `find` and `findByID` operations are transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument.
```ts ```ts
import { CollectionBeforeReadHook } from 'payload/types'; import { CollectionBeforeReadHook } from "payload/types";
const beforeReadHook: CollectionBeforeReadHook = async ({ const beforeReadHook: CollectionBeforeReadHook = async ({
doc, // full document data doc, // full document data
@@ -147,7 +150,7 @@ const beforeReadHook: CollectionBeforeReadHook = async ({
query, // JSON formatted query query, // JSON formatted query
}) => { }) => {
return doc; return doc;
} };
``` ```
### afterRead ### afterRead
@@ -155,7 +158,7 @@ const beforeReadHook: CollectionBeforeReadHook = async ({
Runs as the last step before documents are returned. Flattens locales, hides protected fields, and removes fields that users do not have access to. Runs as the last step before documents are returned. Flattens locales, hides protected fields, and removes fields that users do not have access to.
```ts ```ts
import { CollectionAfterReadHook } from 'payload/types'; import { CollectionAfterReadHook } from "payload/types";
const afterReadHook: CollectionAfterReadHook = async ({ const afterReadHook: CollectionAfterReadHook = async ({
doc, // full document data doc, // full document data
@@ -164,7 +167,7 @@ const afterReadHook: CollectionAfterReadHook = async ({
findMany, // boolean to denote if this hook is running against finding one, or finding many findMany, // boolean to denote if this hook is running against finding one, or finding many
}) => { }) => {
return doc; return doc;
} };
``` ```
### beforeDelete ### beforeDelete
@@ -194,19 +197,37 @@ const afterDeleteHook: CollectionAfterDeleteHook = async ({
}) => {...} }) => {...}
``` ```
### afterOperation
The `afterOperation` hook can be used to modify the result of operations or execute side-effects that run after an operation has completed.
Available Collection operations include `create`, `find`, `findByID`, `update`, `updateByID`, `delete`, `deleteByID`, `login`, `refresh`, and `forgotPassword`.
```ts
import { CollectionAfterOperationHook } from "payload/types";
const afterOperationHook: CollectionAfterOperationHook = async ({
args, // arguments passed into the operation
operation, // name of the operation
result, // the result of the operation, before modifications
}) => {
return result; // return modified result as necessary
};
```
### beforeLogin ### beforeLogin
For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation. For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation.
```ts ```ts
import { CollectionBeforeLoginHook } from 'payload/types'; import { CollectionBeforeLoginHook } from "payload/types";
const beforeLoginHook: CollectionBeforeLoginHook = async ({ const beforeLoginHook: CollectionBeforeLoginHook = async ({
req, // full express request req, // full express request
user, // user being logged in user, // user being logged in
}) => { }) => {
return user; return user;
} };
``` ```
### afterLogin ### afterLogin
@@ -267,7 +288,7 @@ const afterMeHook: CollectionAfterMeHook = async ({
For auth-enabled Collections, this hook runs after successful `forgotPassword` operations. Returned values are discarded. For auth-enabled Collections, this hook runs after successful `forgotPassword` operations. Returned values are discarded.
```ts ```ts
import { CollectionAfterForgotPasswordHook } from 'payload/types'; import { CollectionAfterForgotPasswordHook } from "payload/types";
const afterLoginHook: CollectionAfterForgotPasswordHook = async ({ const afterLoginHook: CollectionAfterForgotPasswordHook = async ({
req, // full express request req, // full express request
@@ -275,7 +296,7 @@ const afterLoginHook: CollectionAfterForgotPasswordHook = async ({
token, // user token token, // user token
}) => { }) => {
return user; return user;
} };
``` ```
## TypeScript ## TypeScript
@@ -298,5 +319,5 @@ import type {
CollectionAfterRefreshHook, CollectionAfterRefreshHook,
CollectionAfterMeHook, CollectionAfterMeHook,
CollectionAfterForgotPasswordHook, CollectionAfterForgotPasswordHook,
} from 'payload/types'; } from "payload/types";
``` ```

View File

@@ -1,6 +1,6 @@
# Payload Draft Preview Example Front-End # Payload Draft Preview Example Front-End
This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview). This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/payload).
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/next-pages). > This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/next-pages).

View File

@@ -20,6 +20,11 @@ export const fetchPage = async (
draft && payloadToken ? '&draft=true' : '' draft && payloadToken ? '&draft=true' : ''
}`, }`,
{ {
method: 'GET',
// this is the key we'll use to on-demand revalidate pages that use this data
// we do this by calling `revalidateTag()` using the same key
// see `app/api/revalidate.ts` for more info
next: { tags: [`pages_${slug}`] },
...(draft && payloadToken ...(draft && payloadToken
? { ? {
headers: { headers: {

View File

@@ -1,23 +1,32 @@
import { revalidatePath } from 'next/cache' import { revalidatePath, revalidateTag } from 'next/cache'
import type { NextRequest } from 'next/server' import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
// this endpoint will revalidate a page by tag or path
// this is to achieve on-demand revalidation of pages that use this data
// send either `collection` and `slug` or `revalidatePath` as query params
export async function GET(request: NextRequest): Promise<unknown> { export async function GET(request: NextRequest): Promise<unknown> {
const path = request.nextUrl.searchParams.get('revalidatePath') const collection = request.nextUrl.searchParams.get('collection')
const slug = request.nextUrl.searchParams.get('slug')
const path = request.nextUrl.searchParams.get('path')
const secret = request.nextUrl.searchParams.get('secret') const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) { if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
return NextResponse.json({ revalidated: false, now: Date.now() }) return NextResponse.json({ revalidated: false, now: Date.now() })
} }
if (typeof collection === 'string' && typeof slug === 'string') {
revalidateTag(`${collection}_${slug}`)
return NextResponse.json({ revalidated: true, now: Date.now() })
}
// there is a known limitation with `revalidatePath` where it will not revalidate exact paths of dynamic routes
// instead, Next.js expects us to revalidate entire directories, i.e. `revalidatePath('/[slug]')` instead of `/example-page`
// for this reason, it is preferred to use `revalidateTag` instead of `revalidatePath`
// - https://github.com/vercel/next.js/issues/49387
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
if (typeof path === 'string') { if (typeof path === 'string') {
// there is a known bug with `revalidatePath` where it will not revalidate exact paths of dynamic routes revalidatePath(path)
// instead, Next.js expects us to revalidate entire directories, i.e. `/[slug]` instead of `/example-page`
// for now we'll make this change but with expectation that it will be fixed so we can use `revalidatePath('/example-page')`
// - https://github.com/vercel/next.js/issues/49387
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
// revalidatePath(path)
revalidatePath('/[slug]')
return NextResponse.json({ revalidated: true, now: Date.now() }) return NextResponse.json({ revalidated: true, now: Date.now() })
} }

View File

@@ -1,6 +1,6 @@
# Payload Draft Preview Example Front-End # Payload Draft Preview Example Front-End
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview). This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/payload).
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/next-app). > This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/next-app).

View File

@@ -6,9 +6,9 @@ const revalidate = async (req: NextApiRequest, res: NextApiResponse): Promise<vo
return res.status(401).json({ message: 'Invalid token' }) return res.status(401).json({ message: 'Invalid token' })
} }
if (typeof req.query.revalidatePath === 'string') { if (typeof req.query.path === 'string') {
try { try {
await res.revalidate(req.query.revalidatePath) await res.revalidate(req.query.path)
return res.json({ revalidated: true }) return res.json({ revalidated: true })
} catch (err: unknown) { } catch (err: unknown) {
// If there was an error, Next.js will continue // If there was an error, Next.js will continue

View File

@@ -1,6 +1,6 @@
# Payload Draft Preview Example # Payload Draft Preview Example
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published. There are various fully working front-ends made explicitly for this example, including: The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/payload) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published. There are various fully working front-ends made explicitly for this example, including:
- [Next.js App Router](../next-app) - [Next.js App Router](../next-app)
- [Next.js Pages Router](../next-pages) - [Next.js Pages Router](../next-pages)

View File

@@ -7,9 +7,11 @@ export const formatAppURL = ({ doc }): string => {
return pathname return pathname
} }
// Revalidate the page in the background, so the user doesn't have to wait // revalidate the page in the background, so the user doesn't have to wait
// Notice that the hook itself is not async and we are not awaiting `revalidate` // notice that the hook itself is not async and we are not awaiting `revalidate`
// Only revalidate existing docs that are published // only revalidate existing docs that are published (not drafts)
// send `revalidatePath`, `collection`, and `slug` to the frontend to use in its revalidate route
// frameworks may have different ways of doing this, but the idea is the same
export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => { export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
if (operation === 'update' && doc._status === 'published') { if (operation === 'update' && doc._status === 'published') {
const url = formatAppURL({ doc }) const url = formatAppURL({ doc })
@@ -17,7 +19,7 @@ export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
const revalidate = async (): Promise<void> => { const revalidate = async (): Promise<void> => {
try { try {
const res = await fetch( const res = await fetch(
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&revalidatePath=${url}`, `${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&collection=pages&slug=${doc?.slug}&path=${url}`,
) )
if (res.ok) { if (res.ok) {

View File

@@ -16,7 +16,7 @@ export const Pages: CollectionConfig = {
formatAppURL({ formatAppURL({
doc, doc,
}), }),
)}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}` )}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
}, },
}, },
versions: { versions: {

View File

@@ -1,6 +1,7 @@
import type { Page } from '../payload-types' import type { Page } from '../payload-types'
export const examplePageDraft: Partial<Page> = { export const examplePageDraft: Partial<Page> = {
title: 'Example Page (Draft)',
richText: [ richText: [
{ {
children: [ children: [

View File

@@ -1,6 +1,6 @@
{ {
"name": "payload", "name": "payload",
"version": "1.13.4", "version": "1.14.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework", "description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -47,6 +47,7 @@
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed", "test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test", "test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:components": "cross-env jest --config=jest.components.config.js", "test:components": "cross-env jest --config=jest.components.config.js",
"translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts",
"clean:cache": "rimraf node_modules/.cache", "clean:cache": "rimraf node_modules/.cache",
"clean": "rimraf dist", "clean": "rimraf dist",
"release:patch": "release-it patch", "release:patch": "release-it patch",

View File

@@ -12,8 +12,8 @@
"devDependencies": { "devDependencies": {
"@types/mongoose-aggregate-paginate-v2": "^1.0.5", "@types/mongoose-aggregate-paginate-v2": "^1.0.5",
"mongodb-memory-server": "^8.13.0", "mongodb-memory-server": "^8.13.0",
"payload": "^1.11.8", "typescript": "^4.9.4",
"typescript": "^4.9.4" "payload": "payloadcms/payload#build/chore/update-2.0"
}, },
"dependencies": { "dependencies": {
"bson-objectid": "^2.0.4", "bson-objectid": "^2.0.4",
@@ -24,4 +24,4 @@
"mongoose-paginate-v2": "1.7.22", "mongoose-paginate-v2": "1.7.22",
"uuid": "^9.0.0" "uuid": "^9.0.0"
} }
} }

View File

@@ -2,8 +2,10 @@ import mongoose from 'mongoose';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import { getLocalizedPaths } from 'payload/dist/database/getLocalizedPaths'; import { getLocalizedPaths } from 'payload/dist/database/getLocalizedPaths';
import { Field, fieldAffectsData } from 'payload/dist/fields/config/types'; import { Field, fieldAffectsData } from 'payload/dist/fields/config/types';
import { PathToQuery, validOperators } from 'payload/dist/database/queryValidation/types'; import { PathToQuery } from 'payload/dist/database/queryValidation/types';
import { validOperators } from 'payload/dist/types/constants';
import { Payload } from 'payload'; import { Payload } from 'payload';
import { Operator } from 'payload/types';
import { operatorMap } from './operatorMap'; import { operatorMap } from './operatorMap';
import { sanitizeQueryValue } from './sanitizeQueryValue'; import { sanitizeQueryValue } from './sanitizeQueryValue';
import { MongooseAdapter } from '..'; import { MongooseAdapter } from '..';
@@ -179,7 +181,7 @@ export async function buildSearchParam({
return relationshipQuery; return relationshipQuery;
} }
if (operator && validOperators.includes(operator)) { if (operator && validOperators.includes(operator as Operator)) {
const operatorKey = operatorMap[operator]; const operatorKey = operatorMap[operator];
if (field.type === 'relationship' || field.type === 'upload') { if (field.type === 'relationship' || field.type === 'upload') {

View File

@@ -10,4 +10,6 @@ export const operatorMap = {
exists: '$exists', exists: '$exists',
equals: '$eq', equals: '$eq',
near: '$near', near: '$near',
within: '$geoWithin',
intersects: '$geoIntersects',
}; };

View File

@@ -5,7 +5,7 @@ import deepmerge from 'deepmerge';
import { Where } from 'payload/types'; import { Where } from 'payload/types';
import { combineMerge } from 'payload/dist/utilities/combineMerge'; import { combineMerge } from 'payload/dist/utilities/combineMerge';
import { Field } from 'payload/dist/fields/config/types'; import { Field } from 'payload/dist/fields/config/types';
import { validOperators } from 'payload/dist/database/queryValidation/types'; import { validOperators } from 'payload/dist/types/constants';
import { Payload } from 'payload'; import { Payload } from 'payload';
import { buildSearchParam } from './buildSearchParams'; import { buildSearchParam } from './buildSearchParams';
import { buildAndOrConditions } from './buildAndOrConditions'; import { buildAndOrConditions } from './buildAndOrConditions';

View File

@@ -95,7 +95,7 @@ export const sanitizeQueryValue = ({ field, path, operator, val, hasCustomID }:
[lng, lat, maxDistance, minDistance] = createArrayFromCommaDelineated(formattedValue); [lng, lat, maxDistance, minDistance] = createArrayFromCommaDelineated(formattedValue);
} }
if (!lng || !lat || (!maxDistance && !minDistance)) { if (lng == null || lat == null || (maxDistance == null && minDistance == null)) {
formattedValue = undefined; formattedValue = undefined;
} else { } else {
formattedValue = { formattedValue = {
@@ -107,6 +107,12 @@ export const sanitizeQueryValue = ({ field, path, operator, val, hasCustomID }:
} }
} }
if (operator === 'within' || operator === 'intersects') {
formattedValue = {
$geometry: formattedValue,
};
}
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) { if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains') { if (operator === 'contains') {
formattedValue = { $regex: formattedValue, $options: 'i' }; formattedValue = { $regex: formattedValue, $options: 'i' };

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,7 @@
"build": "tsc" "build": "tsc"
}, },
"peerDependencies": { "peerDependencies": {
"better-sqlite3": "^8.5.0", "better-sqlite3": "^8.5.0"
"payload": "^1.12.0"
}, },
"dependencies": { "dependencies": {
"@libsql/client": "^0.3.1", "@libsql/client": "^0.3.1",
@@ -25,7 +24,7 @@
"@types/pg": "^8.10.2", "@types/pg": "^8.10.2",
"@types/to-snake-case": "^1.0.0", "@types/to-snake-case": "^1.0.0",
"better-sqlite3": "^8.5.0", "better-sqlite3": "^8.5.0",
"payload": "^1.12.0", "payload": "payloadcms/payload#build/chore/update-2.0",
"typescript": "^4.9.4" "typescript": "^4.9.4"
} }
} }

View File

@@ -3,7 +3,9 @@ import { inArray } from 'drizzle-orm';
import { a as SQL } from 'drizzle-orm/column.d-aa4e525d'; import { a as SQL } from 'drizzle-orm/column.d-aa4e525d';
import { getLocalizedPaths } from 'payload/dist/database/getLocalizedPaths'; import { getLocalizedPaths } from 'payload/dist/database/getLocalizedPaths';
import { Field, fieldAffectsData } from 'payload/dist/fields/config/types'; import { Field, fieldAffectsData } from 'payload/dist/fields/config/types';
import { PathToQuery, validOperators } from 'payload/dist/database/queryValidation/types'; import { PathToQuery } from 'payload/dist/database/queryValidation/types';
import { validOperators } from 'payload/dist/types/constants';
import { Operator } from 'payload/types';
import { operatorMap } from './operatorMap'; import { operatorMap } from './operatorMap';
import { PostgresAdapter } from '../types'; import { PostgresAdapter } from '../types';
@@ -177,7 +179,7 @@ export async function buildSearchParam({
return relationshipQuery; return relationshipQuery;
} }
if (operator && validOperators.includes(operator)) { if (operator && validOperators.includes(operator as Operator)) {
const operatorKey = operatorMap[operator]; const operatorKey = operatorMap[operator];
if (field.type === 'relationship' || field.type === 'upload') { if (field.type === 'relationship' || field.type === 'upload') {

View File

@@ -1,8 +1,8 @@
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { Where } from 'payload/types'; import { Operator, Where } from 'payload/types';
import { Field } from 'payload/dist/fields/config/types'; import { Field } from 'payload/dist/fields/config/types';
import { validOperators } from 'payload/dist/database/queryValidation/types'; import { validOperators } from 'payload/dist/types/constants';
import { and, SQL } from 'drizzle-orm'; import { and, SQL } from 'drizzle-orm';
import { buildSearchParam } from './buildSearchParams'; import { buildSearchParam } from './buildSearchParams';
import { buildAndOrConditions } from './buildAndOrConditions'; import { buildAndOrConditions } from './buildAndOrConditions';
@@ -53,7 +53,7 @@ export async function parseParams({
const pathOperators = where[relationOrPath]; const pathOperators = where[relationOrPath];
if (typeof pathOperators === 'object') { if (typeof pathOperators === 'object') {
for (const operator of Object.keys(pathOperators)) { for (const operator of Object.keys(pathOperators)) {
if (validOperators.includes(operator)) { if (validOperators.includes(operator as Operator)) {
const searchParam = await buildSearchParam({ const searchParam = await buildSearchParam({
collectionSlug, collectionSlug,
globalSlug, globalSlug,

File diff suppressed because it is too large Load Diff

128
scripts/translateNewKeys.ts Normal file
View File

@@ -0,0 +1,128 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-continue */
/* eslint-disable no-restricted-syntax */
import * as fs from 'fs';
import * as path from 'path';
const TRANSLATIONS_DIR = './src/translations';
const SOURCE_LANG_FILE = 'en.json';
const OPENAI_ENDPOINT = 'https://api.openai.com/v1/chat/completions'; // Adjust if needed
const OPENAI_API_KEY = 'sk-YOURKEYHERE'; // Remember to replace with your actual key
async function main() {
const sourceLangContent = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, SOURCE_LANG_FILE), 'utf8'));
const files = fs.readdirSync(TRANSLATIONS_DIR);
for (const file of files) {
if (file === SOURCE_LANG_FILE) {
continue;
}
// check if file ends with .json
if (!file.endsWith('.json')) {
continue;
}
// skip the translation-schema.json file
if (file === 'translation-schema.json') {
continue;
}
console.log('Processing file:', file);
const targetLangContent = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, file), 'utf8'));
const missingKeys = findMissingKeys(sourceLangContent, targetLangContent);
let hasChanged = false;
for (const missingKey of missingKeys) {
const keys = missingKey.split('.');
const sourceText = keys.reduce((acc, key) => acc[key], sourceLangContent);
const targetLang = file.split('.')[0];
const translatedText = await translateText(sourceText, targetLang);
let targetObj = targetLangContent;
for (let i = 0; i < keys.length - 1; i += 1) {
if (!targetObj[keys[i]]) {
targetObj[keys[i]] = {};
}
targetObj = targetObj[keys[i]];
}
targetObj[keys[keys.length - 1]] = translatedText;
hasChanged = true;
}
if (hasChanged) {
const sortedContent = sortKeys(targetLangContent);
fs.writeFileSync(path.join(TRANSLATIONS_DIR, file), JSON.stringify(sortedContent, null, 2));
}
}
}
main().then(() => {
console.log('Translation update completed.');
}).catch((error) => {
console.error('Error occurred:', error);
});
async function translateText(text: string, targetLang: string): Promise<string> {
const response = await fetch(OPENAI_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
max_tokens: 150,
model: 'gpt-4',
messages: [
{
role: 'system',
content: `Only respond with the translation of the text you receive. The original language is English and the translation language is ${targetLang}. Only respond with the translation - do not say anything else. If you cannot translate the text, respond with "[SKIPPED]"`,
},
{
role: 'user',
content: text,
},
],
}),
});
const data = await response.json();
console.log(' Old text:', text, 'New text:', data.choices[0].message.content.trim());
return data.choices[0].message.content.trim();
}
function findMissingKeys(baseObj: any, targetObj: any, prefix = ''): string[] {
let missingKeys = [];
for (const key in baseObj) {
if (typeof baseObj[key] === 'object') {
missingKeys = missingKeys.concat(findMissingKeys(baseObj[key], targetObj[key] || {}, `${prefix}${key}.`));
} else if (!(key in targetObj)) {
missingKeys.push(`${prefix}${key}`);
}
}
return missingKeys;
}
function sortKeys(obj: any): any {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(sortKeys);
}
const sortedKeys = Object.keys(obj).sort();
const sortedObj: { [key: string]: any } = {};
for (const key of sortedKeys) {
sortedObj[key] = sortKeys(obj[key]);
}
return sortedObj;
}

View File

@@ -72,6 +72,7 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>((props,
iconPosition = 'right', iconPosition = 'right',
newTab, newTab,
tooltip, tooltip,
'aria-label': ariaLabel,
} = props; } = props;
const [showTooltip, setShowTooltip] = React.useState(false); const [showTooltip, setShowTooltip] = React.useState(false);
@@ -101,6 +102,8 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>((props,
type, type,
className: classes, className: classes,
disabled, disabled,
'aria-disabled': disabled,
'aria-label': ariaLabel,
onMouseEnter: tooltip ? () => setShowTooltip(true) : undefined, onMouseEnter: tooltip ? () => setShowTooltip(true) : undefined,
onMouseLeave: tooltip ? () => setShowTooltip(false) : undefined, onMouseLeave: tooltip ? () => setShowTooltip(false) : undefined,
onClick: !disabled ? handleClick : undefined, onClick: !disabled ? handleClick : undefined,

View File

@@ -19,4 +19,5 @@ export type Props = {
iconPosition?: 'left' | 'right', iconPosition?: 'left' | 'right',
newTab?: boolean newTab?: boolean
tooltip?: string tooltip?: string
'aria-label'?: string
} }

View File

@@ -5,7 +5,8 @@
padding: base(1.25) $baseline; padding: base(1.25) $baseline;
position: relative; position: relative;
h5 { &__title {
@extend %h5;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -7,7 +7,7 @@ import './index.scss';
const baseClass = 'card'; const baseClass = 'card';
const Card: React.FC<Props> = (props) => { const Card: React.FC<Props> = (props) => {
const { id, title, actions, onClick } = props; const { id, title, titleAs, buttonAriaLabel, actions, onClick } = props;
const classes = [ const classes = [
baseClass, baseClass,
@@ -15,14 +15,16 @@ const Card: React.FC<Props> = (props) => {
onClick && `${baseClass}--has-onclick`, onClick && `${baseClass}--has-onclick`,
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
const Tag = titleAs ?? 'div';
return ( return (
<div <div
className={classes} className={classes}
id={id} id={id}
> >
<h5> <Tag className={`${baseClass}__title`}>
{title} {title}
</h5> </Tag>
{actions && ( {actions && (
<div className={`${baseClass}__actions`}> <div className={`${baseClass}__actions`}>
{actions} {actions}
@@ -30,6 +32,7 @@ const Card: React.FC<Props> = (props) => {
)} )}
{onClick && ( {onClick && (
<Button <Button
aria-label={buttonAriaLabel}
className={`${baseClass}__click`} className={`${baseClass}__click`}
buttonStyle="none" buttonStyle="none"
onClick={onClick} onClick={onClick}

View File

@@ -1,6 +1,10 @@
import { ElementType } from 'react';
export type Props = { export type Props = {
id?: string, id?: string,
title: string, title: string,
titleAs?: ElementType,
buttonAriaLabel?: string,
actions?: React.ReactNode, actions?: React.ReactNode,
onClick?: () => void, onClick?: () => void,
} }

View File

@@ -2,9 +2,9 @@ import React from 'react';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import type { Props } from './types'; import type { Props } from './types';
import { useTheme } from '../../utilities/Theme'; import { useTheme } from '../../utilities/Theme';
import { ShimmerEffect } from '../ShimmerEffect';
import './index.scss'; import './index.scss';
import { ShimmerEffect } from '../ShimmerEffect';
const baseClass = 'code-editor'; const baseClass = 'code-editor';

View File

@@ -1,4 +1,4 @@
import React, { useId, useState } from 'react'; import React, { useId } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Pill from '../Pill'; import Pill from '../Pill';
import Plus from '../../icons/Plus'; import Plus from '../../icons/Plus';
@@ -61,6 +61,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
alignIcon="left" alignIcon="left"
key={`${collection.slug}-${col.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`} key={`${collection.slug}-${col.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`}
icon={active ? <X /> : <Plus />} icon={active ? <X /> : <Plus />}
aria-checked={active}
className={[ className={[
`${baseClass}__column`, `${baseClass}__column`,
active && `${baseClass}__column--active`, active && `${baseClass}__column--active`,

View File

@@ -5,11 +5,11 @@ import { useTranslation } from 'react-i18next';
import { Props, TogglerProps } from './types'; import { Props, TogglerProps } from './types';
import { EditDepthContext, useEditDepth } from '../../utilities/EditDepth'; import { EditDepthContext, useEditDepth } from '../../utilities/EditDepth';
import { Gutter } from '../Gutter'; import { Gutter } from '../Gutter';
import './index.scss';
import X from '../../icons/X'; import X from '../../icons/X';
const baseClass = 'drawer'; import './index.scss';
const baseClass = 'drawer';
const zBase = 100; const zBase = 100;
export const formatDrawerSlug = ({ export const formatDrawerSlug = ({

View File

@@ -38,6 +38,11 @@ const getUseAsTitle = (collection: SanitizedCollectionConfig) => {
return topLevelFields.find((field) => fieldAffectsData(field) && field.name === useAsTitle); return topLevelFields.find((field) => fieldAffectsData(field) && field.name === useAsTitle);
}; };
/**
* The ListControls component is used to render the controls (search, filter, where)
* for a collection's list view. You can find those directly above the table which lists
* the collection's documents.
*/
const ListControls: React.FC<Props> = (props) => { const ListControls: React.FC<Props> = (props) => {
const { const {
collection, collection,
@@ -105,6 +110,8 @@ const ListControls: React.FC<Props> = (props) => {
pillStyle="light" pillStyle="light"
className={`${baseClass}__toggle-columns ${visibleDrawer === 'columns' ? `${baseClass}__buttons-active` : ''}`} className={`${baseClass}__toggle-columns ${visibleDrawer === 'columns' ? `${baseClass}__buttons-active` : ''}`}
onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)} onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)}
aria-expanded={visibleDrawer === 'columns'}
aria-controls={`${baseClass}-columns`}
icon={<Chevron />} icon={<Chevron />}
> >
{t('columns')} {t('columns')}
@@ -114,6 +121,8 @@ const ListControls: React.FC<Props> = (props) => {
pillStyle="light" pillStyle="light"
className={`${baseClass}__toggle-where ${visibleDrawer === 'where' ? `${baseClass}__buttons-active` : ''}`} className={`${baseClass}__toggle-where ${visibleDrawer === 'where' ? `${baseClass}__buttons-active` : ''}`}
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)} onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)}
aria-expanded={visibleDrawer === 'where'}
aria-controls={`${baseClass}-where`}
icon={<Chevron />} icon={<Chevron />}
> >
{t('filters')} {t('filters')}
@@ -123,6 +132,8 @@ const ListControls: React.FC<Props> = (props) => {
className={`${baseClass}__toggle-sort`} className={`${baseClass}__toggle-sort`}
buttonStyle={visibleDrawer === 'sort' ? undefined : 'secondary'} buttonStyle={visibleDrawer === 'sort' ? undefined : 'secondary'}
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)} onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
aria-expanded={visibleDrawer === 'sort'}
aria-controls={`${baseClass}-sort`}
icon="chevron" icon="chevron"
iconStyle="none" iconStyle="none"
> >
@@ -136,6 +147,7 @@ const ListControls: React.FC<Props> = (props) => {
<AnimateHeight <AnimateHeight
className={`${baseClass}__columns`} className={`${baseClass}__columns`}
height={visibleDrawer === 'columns' ? 'auto' : 0} height={visibleDrawer === 'columns' ? 'auto' : 0}
id={`${baseClass}-columns`}
> >
<ColumnSelector collection={collection} /> <ColumnSelector collection={collection} />
</AnimateHeight> </AnimateHeight>
@@ -143,6 +155,7 @@ const ListControls: React.FC<Props> = (props) => {
<AnimateHeight <AnimateHeight
className={`${baseClass}__where`} className={`${baseClass}__where`}
height={visibleDrawer === 'where' ? 'auto' : 0} height={visibleDrawer === 'where' ? 'auto' : 0}
id={`${baseClass}-where`}
> >
<WhereBuilder <WhereBuilder
collection={collection} collection={collection}
@@ -154,6 +167,7 @@ const ListControls: React.FC<Props> = (props) => {
<AnimateHeight <AnimateHeight
className={`${baseClass}__sort`} className={`${baseClass}__sort`}
height={visibleDrawer === 'sort' ? 'auto' : 0} height={visibleDrawer === 'sort' ? 'auto' : 0}
id={`${baseClass}-sort`}
> >
<SortComplex <SortComplex
modifySearchQuery={modifySearchQuery} modifySearchQuery={modifySearchQuery}

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../utilities/Config'; import { useConfig } from '../../utilities/Config';
import RenderCustomComponent from '../../utilities/RenderCustomComponent'; import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import LogOut from '../../icons/LogOut'; import LogOut from '../../icons/LogOut';
@@ -7,16 +8,21 @@ import LogOut from '../../icons/LogOut';
const baseClass = 'nav'; const baseClass = 'nav';
const DefaultLogout = () => { const DefaultLogout = () => {
const { t } = useTranslation('authentication');
const config = useConfig(); const config = useConfig();
const { const {
routes: { admin }, routes: { admin },
admin: { admin: {
logoutRoute, logoutRoute,
components: { logout } components: { logout },
} },
} = config; } = config;
return ( return (
<Link to={`${admin}${logoutRoute}`} className={`${baseClass}__log-out`}> <Link
to={`${admin}${logoutRoute}`}
className={`${baseClass}__log-out`}
aria-label={t('logOut')}
>
<LogOut /> <LogOut />
</Link> </Link>
); );

View File

@@ -24,7 +24,7 @@ const DefaultNav = () => {
const [menuActive, setMenuActive] = useState(false); const [menuActive, setMenuActive] = useState(false);
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
const history = useHistory(); const history = useHistory();
const { i18n } = useTranslation('general'); const { t, i18n } = useTranslation('general');
const { const {
collections, collections,
globals, globals,
@@ -81,6 +81,7 @@ const DefaultNav = () => {
<Link <Link
to={admin} to={admin}
className={`${baseClass}__brand`} className={`${baseClass}__brand`}
aria-label={t('dashboard')}
> >
<Icon /> <Icon />
</Link> </Link>
@@ -141,6 +142,7 @@ const DefaultNav = () => {
<Link <Link
to={`${admin}/account`} to={`${admin}/account`}
className={`${baseClass}__account`} className={`${baseClass}__account`}
aria-label={t('authentication:account')}
> >
<Account /> <Account />
</Link> </Link>

View File

@@ -45,6 +45,10 @@ const StaticPill: React.FC<Props> = (props) => {
children, children,
elementProps, elementProps,
rounded, rounded,
'aria-label': ariaLabel,
'aria-expanded': ariaExpanded,
'aria-controls': ariaControls,
'aria-checked': ariaChecked,
} = props; } = props;
const classes = [ const classes = [
@@ -67,6 +71,10 @@ const StaticPill: React.FC<Props> = (props) => {
return ( return (
<Element <Element
{...elementProps} {...elementProps}
aria-label={ariaLabel}
aria-expanded={ariaExpanded}
aria-controls={ariaControls}
aria-checked={ariaChecked}
className={classes} className={classes}
type={Element === 'button' ? 'button' : undefined} type={Element === 'button' ? 'button' : undefined}
to={to || undefined} to={to || undefined}

View File

@@ -11,6 +11,10 @@ export type Props = {
draggable?: boolean, draggable?: boolean,
rounded?: boolean rounded?: boolean
id?: string id?: string
'aria-label'?: string,
'aria-expanded'?: boolean,
'aria-controls'?: string,
'aria-checked'?: boolean,
elementProps?: HTMLAttributes<HTMLElement> & { elementProps?: HTMLAttributes<HTMLElement> & {
ref: React.RefCallback<HTMLElement> ref: React.RefCallback<HTMLElement>
} }

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { components as SelectComponents, MultiValueProps } from 'react-select'; import { components as SelectComponents, MultiValueProps } from 'react-select';
import type { Option } from '../types'; import type { Option } from '../types';
import './index.scss'; import './index.scss';
const baseClass = 'multi-value-label'; const baseClass = 'multi-value-label';

View File

@@ -4,6 +4,7 @@ import { MultiValueRemoveProps } from 'react-select';
import X from '../../../icons/X'; import X from '../../../icons/X';
import Tooltip from '../../Tooltip'; import Tooltip from '../../Tooltip';
import { Option as OptionType } from '../types'; import { Option as OptionType } from '../types';
import './index.scss'; import './index.scss';
const baseClass = 'multi-value-remove'; const baseClass = 'multi-value-remove';

View File

@@ -45,6 +45,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
components, components,
isCreatable, isCreatable,
selectProps, selectProps,
noOptionsMessage,
} = props; } = props;
const classes = [ const classes = [
@@ -72,6 +73,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
filterOption={filterOption} filterOption={filterOption}
onMenuOpen={onMenuOpen} onMenuOpen={onMenuOpen}
menuPlacement="auto" menuPlacement="auto"
noOptionsMessage={noOptionsMessage}
components={{ components={{
ValueContainer, ValueContainer,
SingleValue, SingleValue,
@@ -134,6 +136,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
inputValue={inputValue} inputValue={inputValue}
onInputChange={(newValue) => setInputValue(newValue)} onInputChange={(newValue) => setInputValue(newValue)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
noOptionsMessage={noOptionsMessage}
components={{ components={{
ValueContainer, ValueContainer,
SingleValue, SingleValue,

View File

@@ -43,6 +43,7 @@ export type OptionGroup = {
} }
export type Props = { export type Props = {
inputId?: string
className?: string className?: string
value?: Option | Option[], value?: Option | Option[],
onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -76,4 +77,5 @@ export type Props = {
*/ */
selectProps?: CustomSelectProps selectProps?: CustomSelectProps
backspaceRemovesValue?: boolean backspaceRemovesValue?: boolean
noOptionsMessage?: (obj: { inputValue: string }) => string
} }

View File

@@ -18,7 +18,7 @@ const SortColumn: React.FC<Props> = (props) => {
} = props; } = props;
const params = useSearchParams(); const params = useSearchParams();
const history = useHistory(); const history = useHistory();
const { i18n } = useTranslation(); const { t, i18n } = useTranslation('general');
const { sort } = params; const { sort } = params;
@@ -50,6 +50,7 @@ const SortColumn: React.FC<Props> = (props) => {
buttonStyle="none" buttonStyle="none"
className={ascClasses.join(' ')} className={ascClasses.join(' ')}
onClick={() => setSort(asc)} onClick={() => setSort(asc)}
aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('ascending') })}
> >
<Chevron /> <Chevron />
</Button> </Button>
@@ -58,6 +59,7 @@ const SortColumn: React.FC<Props> = (props) => {
buttonStyle="none" buttonStyle="none"
className={descClasses.join(' ')} className={descClasses.join(' ')}
onClick={() => setSort(desc)} onClick={() => setSort(desc)}
aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('descending') })}
> >
<Chevron /> <Chevron />
</Button> </Button>

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Props, isComponent } from './types'; import { Props, isComponent } from './types';
import { getTranslation } from '../../../../utilities/getTranslation'; import { getTranslation } from '../../../../utilities/getTranslation';
import './index.scss'; import './index.scss';
const ViewDescription: React.FC<Props> = (props) => { const ViewDescription: React.FC<Props> = (props) => {

View File

@@ -57,6 +57,16 @@ const geo = [
}, },
]; ];
const within = {
label: 'within',
value: 'within',
};
const intersects = {
label: 'intersects',
value: 'intersects',
};
const like = { const like = {
label: 'isLike', label: 'isLike',
value: 'like', value: 'like',
@@ -86,7 +96,7 @@ const fieldTypeConditions = {
}, },
json: { json: {
component: 'Text', component: 'Text',
operators: [...base, like, contains], operators: [...base, like, contains, within, intersects],
}, },
richText: { richText: {
component: 'Text', component: 'Text',
@@ -102,7 +112,7 @@ const fieldTypeConditions = {
}, },
point: { point: {
component: 'Point', component: 'Point',
operators: [...geo], operators: [...geo, within, intersects],
}, },
upload: { upload: {
component: 'Text', component: 'Text',

View File

@@ -13,6 +13,7 @@ import { useSearchParams } from '../../utilities/SearchParams';
import validateWhereQuery from './validateWhereQuery'; import validateWhereQuery from './validateWhereQuery';
import { Where } from '../../../../types'; import { Where } from '../../../../types';
import { getTranslation } from '../../../../utilities/getTranslation'; import { getTranslation } from '../../../../utilities/getTranslation';
import { transformWhereQuery } from './transformWhereQuery';
import './index.scss'; import './index.scss';
@@ -43,6 +44,10 @@ const reduceFields = (fields, i18n) => flattenTopLevelFields(fields).reduce((red
return reduced; return reduced;
}, []); }, []);
/**
* The WhereBuilder component is used to render the filter controls for a collection's list view.
* It is part of the {@link ListControls} component which is used to render the controls (search, filter, where).
*/
const WhereBuilder: React.FC<Props> = (props) => { const WhereBuilder: React.FC<Props> = (props) => {
const { const {
collection, collection,
@@ -59,16 +64,30 @@ const WhereBuilder: React.FC<Props> = (props) => {
const params = useSearchParams(); const params = useSearchParams();
const { t, i18n } = useTranslation('general'); const { t, i18n } = useTranslation('general');
// This handles initializing the where conditions from the search query (URL). That way, if you pass in
// query params to the URL, the where conditions will be initialized from those and displayed in the UI.
// Example: /admin/collections/posts?where[or][0][and][0][text][equals]=example%20post
const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => { const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => {
if (modifySearchQuery && validateWhereQuery(whereFromSearch)) { if (modifySearchQuery && whereFromSearch) {
return whereFromSearch.or; if (validateWhereQuery(whereFromSearch)) {
} return whereFromSearch.or;
}
// Transform the where query to be in the right format. This will transform something simple like [text][equals]=example%20post to the right format
const transformedWhere = transformWhereQuery(whereFromSearch);
if (validateWhereQuery(transformedWhere)) {
return transformedWhere.or;
}
console.warn('Invalid where query in URL. Ignoring.');
}
return []; return [];
}); });
const [reducedFields] = useState(() => reduceFields(collection.fields, i18n)); const [reducedFields] = useState(() => reduceFields(collection.fields, i18n));
// This handles updating the search query (URL) when the where conditions change
useThrottledEffect(() => { useThrottledEffect(() => {
const currentParams = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 }) as { where: Where }; const currentParams = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 }) as { where: Where };
@@ -83,8 +102,11 @@ const WhereBuilder: React.FC<Props> = (props) => {
]; ];
}, []) : []; }, []) : [];
const hasNewWhereConditions = conditions.length > 0;
const newWhereQuery = { const newWhereQuery = {
...typeof currentParams?.where === 'object' ? currentParams.where : {}, ...typeof currentParams?.where === 'object' && (validateWhereQuery(currentParams?.where) || !hasNewWhereConditions) ? currentParams.where : {},
or: [ or: [
...conditions, ...conditions,
...paramsToKeep, ...paramsToKeep,
@@ -94,7 +116,6 @@ const WhereBuilder: React.FC<Props> = (props) => {
if (handleChange) handleChange(newWhereQuery as Where); if (handleChange) handleChange(newWhereQuery as Where);
const hasExistingConditions = typeof currentParams?.where === 'object' && 'or' in currentParams.where; const hasExistingConditions = typeof currentParams?.where === 'object' && 'or' in currentParams.where;
const hasNewWhereConditions = conditions.length > 0;
if (modifySearchQuery && ((hasExistingConditions && !hasNewWhereConditions) || hasNewWhereConditions)) { if (modifySearchQuery && ((hasExistingConditions && !hasNewWhereConditions) || hasNewWhereConditions)) {
history.replace({ history.replace({

View File

@@ -0,0 +1,51 @@
import type { Where } from '../../../../types';
/**
* Something like [or][0][and][0][text][equals]=example%20post will work and pass through the validateWhereQuery check.
* However, something like [text][equals]=example%20post will not work and will fail the validateWhereQuery check,
* even though it is a valid Where query. This needs to be transformed here.
*/
export const transformWhereQuery = (whereQuery): Where => {
if (!whereQuery) {
return {};
}
// Check if 'whereQuery' has 'or' field but no 'and'. This is the case for "correct" queries
if (whereQuery.or && !whereQuery.and) {
return {
or: whereQuery.or.map((query) => {
// ...but if the or query does not have an and, we need to add it
if(!query.and) {
return {
and: [query]
}
}
return query;
}),
};
}
// Check if 'whereQuery' has 'and' field but no 'or'.
if (whereQuery.and && !whereQuery.or) {
return {
or: [
{
and: whereQuery.and,
},
],
};
}
// Check if 'whereQuery' has neither 'or' nor 'and'.
if (!whereQuery.or && !whereQuery.and) {
return {
or: [
{
and: [whereQuery], // top-level siblings are considered 'and'
},
],
};
}
// If 'whereQuery' has 'or' and 'and', just return it as it is.
return whereQuery;
};

View File

@@ -1,8 +1,37 @@
import { Where } from '../../../../types'; import type { Operator, Where } from '../../../../types';
import { validOperators } from '../../../../types/constants';
const validateWhereQuery = (whereQuery): whereQuery is Where => { const validateWhereQuery = (whereQuery): whereQuery is Where => {
if (whereQuery?.or?.length > 0 && whereQuery?.or?.[0]?.and && whereQuery?.or?.[0]?.and?.length > 0) { if (whereQuery?.or?.length > 0 && whereQuery?.or?.[0]?.and && whereQuery?.or?.[0]?.and?.length > 0) {
return true; // At this point we know that the whereQuery has 'or' and 'and' fields,
// now let's check the structure and content of these fields.
const isValid = whereQuery.or.every((orQuery) => {
if (orQuery.and && Array.isArray(orQuery.and)) {
return orQuery.and.every((andQuery) => {
if (typeof andQuery !== 'object') {
return false;
}
const andKeys = Object.keys(andQuery);
// If there are no keys, it's not a valid WhereField.
if (andKeys.length === 0) {
return false;
}
// eslint-disable-next-line no-restricted-syntax
for (const key of andKeys) {
const operator = Object.keys(andQuery[key])[0];
// Check if the key is a valid Operator.
if (!operator || !validOperators.includes(operator as Operator)) {
return false;
}
}
return true;
});
}
return false;
});
return isValid;
} }
return false; return false;

View File

@@ -1,62 +1,74 @@
import React from 'react'; import React from 'react';
import Check from '../../../icons/Check'; import Check from '../../../icons/Check';
import Label from '../../Label'; import Label from '../../Label';
import Line from '../../../icons/Line';
import './index.scss'; import './index.scss';
const baseClass = 'custom-checkbox'; const baseClass = 'custom-checkbox';
type CheckboxInputProps = { type CheckboxInputProps = {
onToggle: React.MouseEventHandler<HTMLButtonElement> onToggle: React.FormEventHandler<HTMLInputElement>
inputRef?: React.MutableRefObject<HTMLInputElement> inputRef?: React.MutableRefObject<HTMLInputElement>
readOnly?: boolean readOnly?: boolean
checked?: boolean checked?: boolean
partialChecked?: boolean
name?: string name?: string
id?: string id?: string
label?: string label?: string
'aria-label'?: string
required?: boolean required?: boolean
} }
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => { export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const { const {
onToggle, onToggle,
checked, checked,
partialChecked,
inputRef, inputRef,
name, name,
id, id,
label, label,
'aria-label': ariaLabel,
readOnly, readOnly,
required, required,
} = props; } = props;
return ( return (
<span <div
className={[ className={[
baseClass, baseClass,
checked && `${baseClass}--checked`, (checked || partialChecked) && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`, readOnly && `${baseClass}--read-only`,
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
> >
<input <div className={`${baseClass}__input`}>
ref={inputRef} <input
id={id} ref={inputRef}
type="checkbox" id={id}
name={name} type="checkbox"
checked={checked} name={name}
readOnly aria-label={ariaLabel}
/> checked={Boolean(checked)}
<button disabled={readOnly}
type="button" onInput={onToggle}
onClick={onToggle} />
> <span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
<span className={`${baseClass}__input`}> {!partialChecked && (
<Check /> <Check />
)}
{partialChecked && (
<Line />
)}
</span> </span>
</div>
{label && (
<Label <Label
htmlFor={id} htmlFor={id}
label={label} label={label}
required={required} required={required}
/> />
</button> )}
</span> </div>
); );
}; };

View File

@@ -4,10 +4,6 @@
position: relative; position: relative;
margin-bottom: $baseline; margin-bottom: $baseline;
input[type=checkbox] {
display: none;
}
.tooltip:not([aria-hidden="true"]) { .tooltip:not([aria-hidden="true"]) {
right: auto; right: auto;
position: relative; position: relative;
@@ -22,16 +18,11 @@
.custom-checkbox { .custom-checkbox {
display: inline-flex;
label { label {
padding-bottom: 0; padding-bottom: 0;
} padding-left: base(.5);
input {
// hidden HTML checkbox
position: absolute;
top: 0;
left: 0;
opacity: 0;
} }
[dir=rtl] &__input { [dir=rtl] &__input {
margin-right: 0; margin-right: 0;
@@ -39,19 +30,76 @@
} }
&__input { &__input {
// visible checkbox
@include formInput; @include formInput;
display: flex;
padding: 0; padding: 0;
line-height: 0; line-height: 0;
position: relative; position: relative;
width: $baseline; width: $baseline;
height: $baseline; height: $baseline;
margin-right: base(.5);
& input[type="checkbox"] {
position: absolute;
// Without the extra 4px, there is an uncheckable area due to the border of the parent element
width: calc(100% + 4px);
height: calc(100% + 4px);
padding: 0;
margin: 0;
margin-left: -2px;
margin-top: -2px;
opacity: 0;
border-radius: 0;
z-index: 1;
cursor: pointer;
}
}
&__icon {
position: absolute;
svg { svg {
opacity: 0; opacity: 0;
} }
} }
&:not(&--read-only) {
&:active,
&:focus-within,
&:focus {
.custom-checkbox__input, & input[type="checkbox"] {
@include inputShadowActive;
outline: 0;
box-shadow: 0 0 3px 3px var(--theme-success-400)!important;
border: 1px solid var(--theme-elevation-150);
}
}
&:hover {
.custom-checkbox__input, & input[type="checkbox"] {
border-color: var(--theme-elevation-250);
}
}
}
&:not(&--read-only):not(&--checked) {
&:hover {
cursor: pointer;
svg {
opacity: 0.2;
}
}
}
&--checked {
.custom-checkbox__icon {
svg {
opacity: 1;
}
}
}
&--read-only { &--read-only {
.custom-checkbox__input { .custom-checkbox__input {
@@ -62,40 +110,6 @@
color: var(--theme-elevation-400); color: var(--theme-elevation-400);
} }
} }
button {
@extend %btn-reset;
display: flex;
align-items: center;
cursor: pointer;
&:focus,
&:active {
outline: none;
}
&:focus {
.custom-checkbox__input {
box-shadow: 0 0 3px 3px var(--theme-success-400);
}
}
&:hover {
svg {
opacity: .2;
}
}
}
&--checked {
button {
.custom-checkbox__input {
svg {
opacity: 1;
}
}
}
}
} }
html[data-theme=light] { html[data-theme=light] {

View File

@@ -88,6 +88,7 @@ const Checkbox: React.FC<Props> = (props) => {
label={getTranslation(label || name, i18n)} label={getTranslation(label || name, i18n)}
name={path} name={path}
checked={Boolean(value)} checked={Boolean(value)}
readOnly={readOnly}
/> />
<FieldDescription <FieldDescription
value={value} value={value}

View File

@@ -10,9 +10,9 @@ import { Props } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation'; import { getTranslation } from '../../../../../utilities/getTranslation';
import { Option } from '../../../elements/ReactSelect/types'; import { Option } from '../../../elements/ReactSelect/types';
import ReactSelect from '../../../elements/ReactSelect'; import ReactSelect from '../../../elements/ReactSelect';
import { isNumber } from '../../../../../utilities/isNumber';
import './index.scss'; import './index.scss';
import { isNumber } from '../../../../../utilities/isNumber';
const NumberField: React.FC<Props> = (props) => { const NumberField: React.FC<Props> = (props) => {
const { const {
@@ -143,9 +143,17 @@ const NumberField: React.FC<Props> = (props) => {
isMulti isMulti
isSortable isSortable
isClearable isClearable
noOptionsMessage={({ inputValue }) => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows;
if (isOverHasMany) {
return t('validation:limitReached', { value: value.length + 1, max: maxRows });
}
return t('general:noOptions');
}}
filterOption={(option, rawInput) => { filterOption={(option, rawInput) => {
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
return isNumber(rawInput) const isOverHasMany = Array.isArray(value) && value.length >= maxRows;
return isNumber(rawInput) && !isOverHasMany;
}} }}
numberOnly numberOnly
/> />

View File

@@ -6,6 +6,7 @@ import Tooltip from '../../../../../elements/Tooltip';
import Edit from '../../../../../icons/Edit'; import Edit from '../../../../../icons/Edit';
import { useAuth } from '../../../../../utilities/Auth'; import { useAuth } from '../../../../../utilities/Auth';
import { Option } from '../../types'; import { Option } from '../../types';
import './index.scss'; import './index.scss';
const baseClass = 'relationship--multi-value-label'; const baseClass = 'relationship--multi-value-label';

View File

@@ -6,11 +6,11 @@ import FormSubmit from '../../../../../Submit';
import { Props } from './types'; import { Props } from './types';
import fieldTypes from '../../../..'; import fieldTypes from '../../../..';
import RenderFields from '../../../../../RenderFields'; import RenderFields from '../../../../../RenderFields';
import './index.scss';
import useHotkey from '../../../../../../../hooks/useHotkey'; import useHotkey from '../../../../../../../hooks/useHotkey';
import { useEditDepth } from '../../../../../../utilities/EditDepth'; import { useEditDepth } from '../../../../../../utilities/EditDepth';
import './index.scss';
const baseClass = 'rich-text-link-edit-modal'; const baseClass = 'rich-text-link-edit-modal';
export const LinkDrawer: React.FC<Props> = ({ export const LinkDrawer: React.FC<Props> = ({

View File

@@ -130,9 +130,11 @@ const DefaultAccount: React.FC<Props> = (props) => {
<h3>{t('general:payloadSettings')}</h3> <h3>{t('general:payloadSettings')}</h3>
<div className={`${baseClass}__language`}> <div className={`${baseClass}__language`}>
<Label <Label
htmlFor="language-select"
label={t('general:language')} label={t('general:language')}
/> />
<ReactSelect <ReactSelect
inputId="language-select"
value={languageOptions.find((language) => (language.value === i18n.language))} value={languageOptions.find((language) => (language.value === i18n.language))}
options={languageOptions} options={languageOptions}
onChange={({ value }) => (i18n.changeLanguage(value))} onChange={({ value }) => (i18n.changeLanguage(value))}

View File

@@ -24,7 +24,7 @@ const Dashboard: React.FC<Props> = (props) => {
} = props; } = props;
const { push } = useHistory(); const { push } = useHistory();
const { i18n } = useTranslation('general'); const { t, i18n } = useTranslation('general');
const { const {
routes: { routes: {
@@ -77,12 +77,14 @@ const Dashboard: React.FC<Props> = (props) => {
<ul className={`${baseClass}__card-list`}> <ul className={`${baseClass}__card-list`}>
{entities.map(({ entity, type }, entityIndex) => { {entities.map(({ entity, type }, entityIndex) => {
let title: string; let title: string;
let buttonAriaLabel: string;
let createHREF: string; let createHREF: string;
let onClick: () => void; let onClick: () => void;
let hasCreatePermission: boolean; let hasCreatePermission: boolean;
if (type === EntityType.collection) { if (type === EntityType.collection) {
title = getTranslation(entity.labels.plural, i18n); title = getTranslation(entity.labels.plural, i18n);
buttonAriaLabel = t('showAllLabel', { label: title });
onClick = () => push({ pathname: `${admin}/collections/${entity.slug}` }); onClick = () => push({ pathname: `${admin}/collections/${entity.slug}` });
createHREF = `${admin}/collections/${entity.slug}/create`; createHREF = `${admin}/collections/${entity.slug}/create`;
hasCreatePermission = permissions?.collections?.[entity.slug]?.create?.permission; hasCreatePermission = permissions?.collections?.[entity.slug]?.create?.permission;
@@ -90,6 +92,7 @@ const Dashboard: React.FC<Props> = (props) => {
if (type === EntityType.global) { if (type === EntityType.global) {
title = getTranslation(entity.label, i18n); title = getTranslation(entity.label, i18n);
buttonAriaLabel = t('editLabel', { label: getTranslation(entity.label, i18n) });
onClick = () => push({ pathname: `${admin}/globals/${entity.slug}` }); onClick = () => push({ pathname: `${admin}/globals/${entity.slug}` });
} }
@@ -97,8 +100,10 @@ const Dashboard: React.FC<Props> = (props) => {
<li key={entityIndex}> <li key={entityIndex}>
<Card <Card
title={title} title={title}
titleAs="h3"
id={`card-${entity.slug}`} id={`card-${entity.slug}`}
onClick={onClick} onClick={onClick}
buttonAriaLabel={buttonAriaLabel}
actions={(hasCreatePermission && type === EntityType.collection) ? ( actions={(hasCreatePermission && type === EntityType.collection) ? (
<Button <Button
el="link" el="link"
@@ -107,6 +112,7 @@ const Dashboard: React.FC<Props> = (props) => {
round round
buttonStyle="icon-label" buttonStyle="icon-label"
iconStyle="with-border" iconStyle="with-border"
aria-label={t('createNewLabel', { label: getTranslation(entity.labels.singular, i18n) })}
/> />
) : undefined} ) : undefined}
/> />

View File

@@ -11,7 +11,6 @@ import FormSubmit from '../../forms/Submit';
import Button from '../../elements/Button'; import Button from '../../elements/Button';
import Meta from '../../utilities/Meta'; import Meta from '../../utilities/Meta';
import './index.scss'; import './index.scss';
const baseClass = 'forgot-password'; const baseClass = 'forgot-password';

View File

@@ -13,7 +13,6 @@ import Button from '../../elements/Button';
import Meta from '../../utilities/Meta'; import Meta from '../../utilities/Meta';
import HiddenInput from '../../forms/field-types/HiddenInput'; import HiddenInput from '../../forms/field-types/HiddenInput';
import './index.scss'; import './index.scss';
const baseClass = 'reset-password'; const baseClass = 'reset-password';

View File

@@ -100,7 +100,10 @@ const DefaultList: React.FC<Props> = (props) => {
{getTranslation(pluralLabel, i18n)} {getTranslation(pluralLabel, i18n)}
</h1> </h1>
{hasCreatePermission && ( {hasCreatePermission && (
<Pill to={newDocumentURL}> <Pill
to={newDocumentURL}
aria-label={t('createNewLabel', { label: getTranslation(singularLabel, i18n) })}
>
{t('createNew')} {t('createNew')}
</Pill> </Pill>
)} )}

View File

@@ -1,34 +0,0 @@
@import '../../../../../scss/styles.scss';
.select-all {
button {
@extend %btn-reset;
display: flex;
align-items: center;
cursor: pointer;
&:focus:not(:focus-visible),
&:active {
outline: none;
}
&:hover {
svg {
opacity: .2;
}
}
&:focus-visible {
outline-offset: var(--accessibility-outline-offset);
}
}
&__input {
@include formInput;
padding: 0;
line-height: 0;
position: relative;
width: $baseline;
height: $baseline;
}
}

View File

@@ -1,31 +1,22 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { SelectAllStatus, useSelection } from '../SelectionProvider'; import { SelectAllStatus, useSelection } from '../SelectionProvider';
import Check from '../../../../icons/Check';
import Line from '../../../../icons/Line';
import './index.scss'; import { CheckboxInput } from '../../../../forms/field-types/Checkbox/Input';
const baseClass = 'select-all';
const SelectAll: React.FC = () => { const SelectAll: React.FC = () => {
const { t } = useTranslation('general');
const { selectAll, toggleAll } = useSelection(); const { selectAll, toggleAll } = useSelection();
return ( return (
<div className={baseClass}> <CheckboxInput
<button id="select-all"
type="button" aria-label={selectAll === SelectAllStatus.None ? t('selectAllRows') : t('deselectAllRows')}
onClick={() => toggleAll()} checked={selectAll === SelectAllStatus.AllInPage || selectAll === SelectAllStatus.AllAvailable}
> partialChecked={selectAll === SelectAllStatus.Some}
<span className={`${baseClass}__input`}> onToggle={() => toggleAll()}
{ (selectAll === SelectAllStatus.AllInPage || selectAll === SelectAllStatus.AllAvailable) && ( />
<Check />
)}
{ selectAll === SelectAllStatus.Some && (
<Line />
)}
</span>
</button>
</div>
); );
}; };

View File

@@ -1,31 +1,17 @@
import React from 'react'; import React from 'react';
import { useSelection } from '../SelectionProvider'; import { useSelection } from '../SelectionProvider';
import Check from '../../../../icons/Check'; import { CheckboxInput } from '../../../../forms/field-types/Checkbox/Input';
import './index.scss'; import './index.scss';
const baseClass = 'select-row';
const SelectRow: React.FC<{ id: string | number }> = ({ id }) => { const SelectRow: React.FC<{ id: string | number }> = ({ id }) => {
const { selected, setSelection } = useSelection(); const { selected, setSelection } = useSelection();
return ( return (
<div <CheckboxInput
className={[ checked={selected[id]}
baseClass, onToggle={() => setSelection(id)}
(selected[id]) && `${baseClass}--checked`, />
].filter(Boolean).join(' ')}
key={id}
>
<button
type="button"
onClick={() => setSelection(id)}
>
<span className={`${baseClass}__input`}>
<Check />
</span>
</button>
</div>
); );
}; };

View File

@@ -16,6 +16,12 @@ import { useSearchParams } from '../../../utilities/SearchParams';
import { TableColumnsProvider } from '../../../elements/TableColumns'; import { TableColumnsProvider } from '../../../elements/TableColumns';
import type { Field } from '../../../../../fields/config/types'; import type { Field } from '../../../../../fields/config/types';
/**
* The ListView component is table which lists the collection's documents.
* The default list view can be found at the {@link DefaultList} component.
* Users can also create pass their own custom list view component instead
* of using the default one.
*/
const ListView: React.FC<ListIndexProps> = (props) => { const ListView: React.FC<ListIndexProps> = (props) => {
const { const {
collection, collection,

View File

@@ -2,6 +2,7 @@ import crypto from 'crypto';
import { APIError } from '../../errors'; import { APIError } from '../../errors';
import { PayloadRequest } from '../../express/types'; import { PayloadRequest } from '../../express/types';
import { Collection } from '../../collections/config/types'; import { Collection } from '../../collections/config/types';
import { buildAfterOperation } from '../../collections/operations/utils';
export type Arguments = { export type Arguments = {
collection: Collection collection: Collection
@@ -134,6 +135,16 @@ async function forgotPassword(incomingArgs: Arguments): Promise<string | null> {
await hook({ args, context: req.context }); await hook({ args, context: req.context });
}, Promise.resolve()); }, Promise.resolve());
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
token = await buildAfterOperation({
operation: 'forgotPassword',
args,
result: token,
});
return token; return token;
} }

View File

@@ -10,6 +10,7 @@ import { User } from '../types';
import { Collection } from '../../collections/config/types'; import { Collection } from '../../collections/config/types';
import { afterRead } from '../../fields/hooks/afterRead'; import { afterRead } from '../../fields/hooks/afterRead';
import unlock from './unlock'; import unlock from './unlock';
import { buildAfterOperation } from '../../collections/operations/utils';
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts'; import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts';
import { authenticateLocalStrategy } from '../strategies/local/authenticate'; import { authenticateLocalStrategy } from '../strategies/local/authenticate';
import { getFieldsToSign } from './getFieldsToSign'; import { getFieldsToSign } from './getFieldsToSign';
@@ -240,16 +241,29 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
}) || user; }) || user;
}, Promise.resolve()); }, Promise.resolve());
// ///////////////////////////////////// let result: Result & { user: GeneratedTypes['collections'][TSlug] } = {
// Return results
// /////////////////////////////////////
if (shouldCommit) await payload.db.commitTransaction(req.transactionID);
return {
token, token,
user, user,
exp: (jwt.decode(token) as jwt.JwtPayload).exp, exp: (jwt.decode(token) as jwt.JwtPayload).exp,
}; };
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
operation: 'login',
args,
result,
});
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
if (shouldCommit) await payload.db.commitTransaction(req.transactionID);
return result;
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(req); await killTransaction(req);
throw error; throw error;

View File

@@ -1,11 +1,12 @@
import url from 'url';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Response } from 'express'; import { Response } from 'express';
import url from 'url';
import { Collection, BeforeOperationHook } from '../../collections/config/types'; import { Collection, BeforeOperationHook } from '../../collections/config/types';
import { Forbidden } from '../../errors'; import { Forbidden } from '../../errors';
import getCookieExpiration from '../../utilities/getCookieExpiration'; import getCookieExpiration from '../../utilities/getCookieExpiration';
import { Document } from '../../types'; import { Document } from '../../types';
import { PayloadRequest } from '../../express/types'; import { PayloadRequest } from '../../express/types';
import { buildAfterOperation } from '../../collections/operations/utils';
import { getFieldsToSign } from './getFieldsToSign'; import { getFieldsToSign } from './getFieldsToSign';
export type Result = { export type Result = {
@@ -97,7 +98,7 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
args.res.cookie(`${config.cookiePrefix}-token`, refreshedToken, cookieOptions); args.res.cookie(`${config.cookiePrefix}-token`, refreshedToken, cookieOptions);
} }
let response: Result = { let result: Result = {
user, user,
refreshedToken, refreshedToken,
exp, exp,
@@ -110,20 +111,31 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => { await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => {
await priorHook; await priorHook;
response = (await hook({ result = (await hook({
req: args.req, req: args.req,
res: args.res, res: args.res,
exp, exp,
token: refreshedToken, token: refreshedToken,
context: args.req.context, context: args.req.context,
})) || response; })) || result;
}, Promise.resolve()); }, Promise.resolve());
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation({
operation: 'refresh',
args,
result,
});
// ///////////////////////////////////// // /////////////////////////////////////
// Return results // Return results
// ///////////////////////////////////// // /////////////////////////////////////
return response; return result;
} }
export default refresh; export default refresh;

View File

@@ -17,7 +17,7 @@ const swcOptions = {
tsx: true, tsx: true,
}, },
paths: undefined, paths: undefined,
baseUrl: __dirname, baseUrl: path.resolve(),
}, },
module: { module: {
type: 'commonjs', type: 'commonjs',

View File

@@ -29,6 +29,7 @@ export const defaults = {
afterRead: [], afterRead: [],
beforeDelete: [], beforeDelete: [],
afterDelete: [], afterDelete: [],
afterOperation: [],
beforeLogin: [], beforeLogin: [],
afterLogin: [], afterLogin: [],
afterLogout: [], afterLogout: [],

View File

@@ -103,6 +103,7 @@ const collectionSchema = joi.object().keys({
afterRead: joi.array().items(joi.func()), afterRead: joi.array().items(joi.func()),
beforeDelete: joi.array().items(joi.func()), beforeDelete: joi.array().items(joi.func()),
afterDelete: joi.array().items(joi.func()), afterDelete: joi.array().items(joi.func()),
afterOperation: joi.array().items(joi.func()),
beforeLogin: joi.array().items(joi.func()), beforeLogin: joi.array().items(joi.func()),
afterLogin: joi.array().items(joi.func()), afterLogin: joi.array().items(joi.func()),
afterLogout: joi.array().items(joi.func()), afterLogout: joi.array().items(joi.func()),

View File

@@ -15,6 +15,7 @@ import {
CustomSaveButtonProps, CustomSaveButtonProps,
CustomSaveDraftButtonProps, CustomSaveDraftButtonProps,
} from '../../admin/components/elements/types'; } from '../../admin/components/elements/types';
import { AfterOperationArg, AfterOperationMap } from '../operations/utils';
import type { Props as ListProps } from '../../admin/components/views/collections/List/types'; import type { Props as ListProps } from '../../admin/components/views/collections/List/types';
import type { Props as EditProps } from '../../admin/components/views/collections/Edit/types'; import type { Props as EditProps } from '../../admin/components/views/collections/Edit/types';
@@ -110,6 +111,13 @@ export type AfterDeleteHook<T extends TypeWithID = any> = (args: {
context: RequestContext; context: RequestContext;
}) => any; }) => any;
export type AfterOperationHook<
T extends TypeWithID = any,
> = (
arg: AfterOperationArg<T>,
) => Promise<ReturnType<AfterOperationMap<T>[keyof AfterOperationMap<T>]>>;
export type AfterErrorHook = (err: Error, res: unknown, context: RequestContext) => { response: any, status: number } | void; export type AfterErrorHook = (err: Error, res: unknown, context: RequestContext) => { response: any, status: number } | void;
export type BeforeLoginHook<T extends TypeWithID = any> = (args: { export type BeforeLoginHook<T extends TypeWithID = any> = (args: {
@@ -301,6 +309,7 @@ export type CollectionConfig = {
afterMe?: AfterMeHook[]; afterMe?: AfterMeHook[];
afterRefresh?: AfterRefreshHook[]; afterRefresh?: AfterRefreshHook[];
afterForgotPassword?: AfterForgotPasswordHook[]; afterForgotPassword?: AfterForgotPasswordHook[];
afterOperation?: AfterOperationHook[];
}; };
/** /**
* Custom rest api endpoints, set false to disable all rest endpoints for this collection. * Custom rest api endpoints, set false to disable all rest endpoints for this collection.

View File

@@ -22,13 +22,16 @@ import { afterRead } from '../../fields/hooks/afterRead';
import { generateFileData } from '../../uploads/generateFileData'; import { generateFileData } from '../../uploads/generateFileData';
import { saveVersion } from '../../versions/saveVersion'; import { saveVersion } from '../../versions/saveVersion';
import { mapAsync } from '../../utilities/mapAsync'; import { mapAsync } from '../../utilities/mapAsync';
import { buildAfterOperation } from './utils';
import { registerLocalStrategy } from '../../auth/strategies/local/register'; import { registerLocalStrategy } from '../../auth/strategies/local/register';
import { initTransaction } from '../../utilities/initTransaction'; import { initTransaction } from '../../utilities/initTransaction';
import { killTransaction } from '../../utilities/killTransaction'; import { killTransaction } from '../../utilities/killTransaction';
const unlinkFile = promisify(fs.unlink); const unlinkFile = promisify(fs.unlink);
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = { export type CreateUpdateType = { [field: string | number | symbol]: unknown }
export type Arguments<T extends CreateUpdateType> = {
collection: Collection collection: Collection
req: PayloadRequest req: PayloadRequest
depth?: number depth?: number
@@ -349,6 +352,17 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
}) || result; }) || result;
}, Promise.resolve()); }, Promise.resolve());
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
operation: 'create',
args,
result,
});
// Remove temp files if enabled, as express-fileupload does not do this automatically // Remove temp files if enabled, as express-fileupload does not do this automatically
if (config.upload?.useTempFiles && collectionConfig.upload) { if (config.upload?.useTempFiles && collectionConfig.upload) {
const { files } = req; const { files } = req;

View File

@@ -14,6 +14,7 @@ import { validateQueryPaths } from '../../database/queryValidation/validateQuery
import { combineQueries } from '../../database/combineQueries'; import { combineQueries } from '../../database/combineQueries';
import { initTransaction } from '../../utilities/initTransaction'; import { initTransaction } from '../../utilities/initTransaction';
import { killTransaction } from '../../utilities/killTransaction'; import { killTransaction } from '../../utilities/killTransaction';
import { buildAfterOperation } from './utils';
export type Arguments = { export type Arguments = {
depth?: number depth?: number
@@ -247,12 +248,24 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
req, req,
}); });
if (shouldCommit) await payload.db.commitTransaction(req.transactionID); let result = {
return {
docs: awaitedDocs.filter(Boolean), docs: awaitedDocs.filter(Boolean),
errors, errors,
}; };
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
operation: 'delete',
args,
result,
});
if (shouldCommit) await payload.db.commitTransaction(req.transactionID);
return result;
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(req); await killTransaction(req);
throw error; throw error;

View File

@@ -12,6 +12,7 @@ import { combineQueries } from '../../database/combineQueries';
import { deleteUserPreferences } from '../../preferences/deleteUserPreferences'; import { deleteUserPreferences } from '../../preferences/deleteUserPreferences';
import { killTransaction } from '../../utilities/killTransaction'; import { killTransaction } from '../../utilities/killTransaction';
import { initTransaction } from '../../utilities/initTransaction'; import { initTransaction } from '../../utilities/initTransaction';
import { buildAfterOperation } from './utils';
export type Arguments = { export type Arguments = {
depth?: number depth?: number
@@ -192,6 +193,16 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(inc
}) || result; }) || result;
}, Promise.resolve()); }, Promise.resolve());
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
operation: 'deleteByID',
args,
result,
});
// ///////////////////////////////////// // /////////////////////////////////////
// 8. Return results // 8. Return results
// ///////////////////////////////////// // /////////////////////////////////////

View File

@@ -11,6 +11,7 @@ import { buildVersionCollectionFields } from '../../versions/buildCollectionFiel
import { combineQueries } from '../../database/combineQueries'; import { combineQueries } from '../../database/combineQueries';
import { initTransaction } from '../../utilities/initTransaction'; import { initTransaction } from '../../utilities/initTransaction';
import { killTransaction } from '../../utilities/killTransaction'; import { killTransaction } from '../../utilities/killTransaction';
import { buildAfterOperation } from './utils';
export type Arguments = { export type Arguments = {
collection: Collection collection: Collection
@@ -234,6 +235,16 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
})), })),
}; };
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<T>({
operation: 'find',
args,
result,
});
// ///////////////////////////////////// // /////////////////////////////////////
// Return results // Return results
// ///////////////////////////////////// // /////////////////////////////////////

View File

@@ -10,6 +10,7 @@ import { combineQueries } from '../../database/combineQueries';
import type { FindOneArgs } from '../../database/types'; import type { FindOneArgs } from '../../database/types';
import { initTransaction } from '../../utilities/initTransaction'; import { initTransaction } from '../../utilities/initTransaction';
import { killTransaction } from '../../utilities/killTransaction'; import { killTransaction } from '../../utilities/killTransaction';
import { buildAfterOperation } from './utils';
export type Arguments = { export type Arguments = {
collection: Collection collection: Collection
@@ -192,6 +193,16 @@ async function findByID<T extends TypeWithID>(
}) || result; }) || result;
}, Promise.resolve()); }, Promise.resolve());
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<T>({
operation: 'findByID',
args,
result: result as any,
}); // TODO: fix this typing
// ///////////////////////////////////// // /////////////////////////////////////
// Return results // Return results
// ///////////////////////////////////// // /////////////////////////////////////

View File

@@ -22,8 +22,10 @@ import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQu
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields'; import { buildVersionCollectionFields } from '../../versions/buildCollectionFields';
import { initTransaction } from '../../utilities/initTransaction'; import { initTransaction } from '../../utilities/initTransaction';
import { killTransaction } from '../../utilities/killTransaction'; import { killTransaction } from '../../utilities/killTransaction';
import { CreateUpdateType } from './create';
import { buildAfterOperation } from './utils';
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = { export type Arguments<T extends CreateUpdateType> = {
collection: Collection collection: Collection
req: PayloadRequest req: PayloadRequest
where: Where where: Where
@@ -361,6 +363,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
collectionConfig, collectionConfig,
}); });
// ///////////////////////////////////// // /////////////////////////////////////
// Return results // Return results
// ///////////////////////////////////// // /////////////////////////////////////
@@ -377,12 +380,24 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
const awaitedDocs = await Promise.all(promises); const awaitedDocs = await Promise.all(promises);
if (shouldCommit) await payload.db.commitTransaction(req.transactionID); let result = {
return {
docs: awaitedDocs.filter(Boolean), docs: awaitedDocs.filter(Boolean),
errors, errors,
}; };
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
operation: 'update',
args,
result,
});
if (shouldCommit) await payload.db.commitTransaction(req.transactionID);
return result;
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(req); await killTransaction(req);
throw error; throw error;

View File

@@ -16,6 +16,7 @@ import { generateFileData } from '../../uploads/generateFileData';
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion'; import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion';
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles'; import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles';
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles'; import { unlinkTempFiles } from '../../uploads/unlinkTempFiles';
import { buildAfterOperation } from './utils';
import { generatePasswordSaltHash } from '../../auth/strategies/local/generatePasswordSaltHash'; import { generatePasswordSaltHash } from '../../auth/strategies/local/generatePasswordSaltHash';
import { combineQueries } from '../../database/combineQueries'; import { combineQueries } from '../../database/combineQueries';
import type { FindOneArgs } from '../../database/types'; import type { FindOneArgs } from '../../database/types';
@@ -341,6 +342,16 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
}) || result; }) || result;
}, Promise.resolve()); }, Promise.resolve());
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
operation: 'updateByID',
args,
result,
});
await unlinkTempFiles({ await unlinkTempFiles({
req, req,
config, config,

View File

@@ -0,0 +1,71 @@
import find from './find';
import update from './update';
import deleteOperation from './delete';
import create from './create';
import login from '../../auth/operations/login';
import refresh from '../../auth/operations/refresh';
import findByID from './findByID';
import updateByID from './updateByID';
import deleteByID from './deleteByID';
import { AfterOperationHook, TypeWithID } from '../config/types';
import forgotPassword from '../../auth/operations/forgotPassword';
export type AfterOperationMap<
T extends TypeWithID,
> = {
create: typeof create, // todo: pass correct generic
find: typeof find<T>,
findByID: typeof findByID<T>,
update: typeof update, // todo: pass correct generic
updateByID: typeof updateByID, // todo: pass correct generic
delete: typeof deleteOperation, // todo: pass correct generic
deleteByID: typeof deleteByID, // todo: pass correct generic
login: typeof login,
refresh: typeof refresh,
forgotPassword: typeof forgotPassword,
}
export type AfterOperationArg<T extends TypeWithID> =
| { operation: 'create'; result: Awaited<ReturnType<AfterOperationMap<T>['create']>>, args: Parameters<AfterOperationMap<T>['create']>[0] }
| { operation: 'find'; result: Awaited<ReturnType<AfterOperationMap<T>['find']>>, args: Parameters<AfterOperationMap<T>['find']>[0] }
| { operation: 'findByID'; result: Awaited<ReturnType<AfterOperationMap<T>['findByID']>>, args: Parameters<AfterOperationMap<T>['findByID']>[0] }
| { operation: 'update'; result: Awaited<ReturnType<AfterOperationMap<T>['update']>>, args: Parameters<AfterOperationMap<T>['update']>[0] }
| { operation: 'updateByID'; result: Awaited<ReturnType<AfterOperationMap<T>['updateByID']>>, args: Parameters<AfterOperationMap<T>['updateByID']>[0] }
| { operation: 'delete'; result: Awaited<ReturnType<AfterOperationMap<T>['delete']>>, args: Parameters<AfterOperationMap<T>['delete']>[0] }
| { operation: 'deleteByID'; result: Awaited<ReturnType<AfterOperationMap<T>['deleteByID']>>, args: Parameters<AfterOperationMap<T>['deleteByID']>[0] }
| { operation: 'login'; result: Awaited<ReturnType<AfterOperationMap<T>['login']>>, args: Parameters<AfterOperationMap<T>['login']>[0] }
| { operation: 'refresh'; result: Awaited<ReturnType<AfterOperationMap<T>['refresh']>>, args: Parameters<AfterOperationMap<T>['refresh']>[0] }
| { operation: 'forgotPassword'; result: Awaited<ReturnType<AfterOperationMap<T>['forgotPassword']>>, args: Parameters<AfterOperationMap<T>['forgotPassword']>[0] };
// export type AfterOperationHook = typeof buildAfterOperation;
export const buildAfterOperation = async <
T extends TypeWithID = any,
O extends keyof AfterOperationMap<T> = keyof AfterOperationMap<T>
>
(
operationArgs: AfterOperationArg<T> & { operation: O },
): Promise<Awaited<ReturnType<AfterOperationMap<T>[O]>>> => {
const {
operation,
args,
result,
} = operationArgs;
let newResult = result;
await args.collection.config.hooks.afterOperation.reduce(async (priorHook, hook: AfterOperationHook<T>) => {
await priorHook;
const hookResult = await hook({
operation,
args,
result: newResult,
} as AfterOperationArg<T>);
if (hookResult !== undefined) {
newResult = hookResult;
}
}, Promise.resolve());
return newResult;
};

View File

@@ -11,48 +11,47 @@ import getPreferencesCollection from '../preferences/preferencesCollection';
import { migrationsCollection } from '../database/migrations/migrationsCollection'; import { migrationsCollection } from '../database/migrations/migrationsCollection';
import getDefaultBundler from '../bundlers/webpack/bundler'; import getDefaultBundler from '../bundlers/webpack/bundler';
const sanitizeAdmin = (config: SanitizedConfig): SanitizedConfig['admin'] => { const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig> => {
const adminConfig = config.admin; const sanitizedConfig = { ...configToSanitize };
// add default user collection if none provided // add default user collection if none provided
if (!adminConfig?.user) { if (!sanitizedConfig?.admin?.user) {
const firstCollectionWithAuth = config.collections.find(({ auth }) => Boolean(auth)); const firstCollectionWithAuth = sanitizedConfig.collections.find(({ auth }) => Boolean(auth));
if (firstCollectionWithAuth) { if (firstCollectionWithAuth) {
adminConfig.user = firstCollectionWithAuth.slug; sanitizedConfig.admin.user = firstCollectionWithAuth.slug;
} else { } else {
adminConfig.user = 'users'; sanitizedConfig.admin.user = defaultUserCollection.slug;
const sanitizedDefaultUser = sanitizeCollection(config, defaultUserCollection); sanitizedConfig.collections.push(defaultUserCollection);
config.collections.push(sanitizedDefaultUser);
} }
} }
if (!config.collections.find(({ slug }) => slug === adminConfig.user)) { if (!sanitizedConfig.collections.find(({ slug }) => slug === sanitizedConfig.admin.user)) {
throw new InvalidConfiguration(`${config.admin.user} is not a valid admin user collection`); throw new InvalidConfiguration(`${sanitizedConfig.admin.user} is not a valid admin user collection`);
} }
// add default bundler if none provided // add default bundler if none provided
if (!adminConfig.bundler) { if (!sanitizedConfig.admin.bundler) {
adminConfig.bundler = getDefaultBundler(); sanitizedConfig.admin.bundler = getDefaultBundler();
} }
return adminConfig; return sanitizedConfig as Partial<SanitizedConfig>;
}; };
export const sanitizeConfig = (config: Config): SanitizedConfig => { export const sanitizeConfig = (incomingConfig: Config): SanitizedConfig => {
const sanitizedConfig: Config = merge(defaults, config, { const configWithDefaults: Config = merge(defaults, incomingConfig, {
isMergeableObject: isPlainObject, isMergeableObject: isPlainObject,
}) as Config; }) as Config;
sanitizedConfig.admin = sanitizeAdmin(sanitizedConfig as SanitizedConfig); const config: Partial<SanitizedConfig> = sanitizeAdminConfig(configWithDefaults);
if (sanitizedConfig.localization && sanitizedConfig.localization.locales?.length > 0) { if (config.localization && config.localization.locales?.length > 0) {
// clone localization config so to not break everything // clone localization config so to not break everything
const firstLocale = sanitizedConfig.localization.locales[0]; const firstLocale = config.localization.locales[0];
if (typeof firstLocale === 'string') { if (typeof firstLocale === 'string') {
(sanitizedConfig.localization as SanitizedLocalizationConfig).localeCodes = [...(sanitizedConfig.localization as LocalizationConfigWithNoLabels).locales]; (config.localization as SanitizedLocalizationConfig).localeCodes = [...(config.localization as unknown as LocalizationConfigWithNoLabels).locales];
// is string[], so convert to Locale[] // is string[], so convert to Locale[]
(sanitizedConfig.localization as SanitizedLocalizationConfig).locales = (sanitizedConfig.localization as LocalizationConfigWithNoLabels).locales.map((locale) => ({ (config.localization as SanitizedLocalizationConfig).locales = (config.localization as unknown as LocalizationConfigWithNoLabels).locales.map((locale) => ({
label: locale, label: locale,
code: locale, code: locale,
rtl: false, rtl: false,
@@ -60,35 +59,36 @@ export const sanitizeConfig = (config: Config): SanitizedConfig => {
})); }));
} else { } else {
// is Locale[], so convert to string[] for localeCodes // is Locale[], so convert to string[] for localeCodes
(sanitizedConfig.localization as SanitizedLocalizationConfig).localeCodes = (sanitizedConfig.localization as SanitizedLocalizationConfig).locales.reduce((locales, locale) => { (config.localization as SanitizedLocalizationConfig).localeCodes = (config.localization as SanitizedLocalizationConfig).locales.reduce((locales, locale) => {
locales.push(locale.code); locales.push(locale.code);
return locales; return locales;
}, [] as string[]); }, [] as string[]);
(sanitizedConfig.localization as SanitizedLocalizationConfig).locales = (sanitizedConfig.localization as LocalizationConfigWithLabels).locales.map((locale) => ({ (config.localization as SanitizedLocalizationConfig).locales = (config.localization as LocalizationConfigWithLabels).locales.map((locale) => ({
...locale, ...locale,
toString: () => locale.code, toString: () => locale.code,
})); }));
} }
} }
sanitizedConfig.collections.push(getPreferencesCollection(sanitizedConfig));
sanitizedConfig.collections.push(migrationsCollection);
sanitizedConfig.collections = sanitizedConfig.collections.map((collection) => sanitizeCollection(sanitizedConfig, collection)); configWithDefaults.collections.push(getPreferencesCollection(configWithDefaults));
checkDuplicateCollections(sanitizedConfig.collections); configWithDefaults.collections.push(migrationsCollection);
if (sanitizedConfig.globals.length > 0) { config.collections = config.collections.map((collection) => sanitizeCollection(configWithDefaults, collection));
sanitizedConfig.globals = sanitizeGlobals(sanitizedConfig.collections, sanitizedConfig.globals); checkDuplicateCollections(config.collections);
if (config.globals.length > 0) {
config.globals = sanitizeGlobals(config.collections, config.globals);
} }
if (typeof sanitizedConfig.serverURL === 'undefined') { if (typeof config.serverURL === 'undefined') {
sanitizedConfig.serverURL = ''; config.serverURL = '';
} }
if (sanitizedConfig.serverURL !== '') { if (config.serverURL !== '') {
sanitizedConfig.csrf.push(sanitizedConfig.serverURL); config.csrf.push(config.serverURL);
} }
return sanitizedConfig as SanitizedConfig; return config as SanitizedConfig;
}; };

View File

@@ -1,8 +1,6 @@
import { CollectionPermission, GlobalPermission } from '../../auth'; import { CollectionPermission, GlobalPermission } from '../../auth';
import { Field, FieldAffectingData, TabAsField, UIField } from '../../fields/config/types'; import { Field, FieldAffectingData, TabAsField, UIField } from '../../fields/config/types';
export const validOperators = ['like', 'contains', 'in', 'all', 'not_in', 'greater_than_equal', 'greater_than', 'less_than_equal', 'less_than', 'not_equals', 'equals', 'exists', 'near'];
export type EntityPolicies = { export type EntityPolicies = {
collections?: { collections?: {
[collectionSlug: string]: CollectionPermission; [collectionSlug: string]: CollectionPermission;

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { PayloadRequest, Where, WhereField } from '../../types'; import { Operator, PayloadRequest, Where } from '../../types';
import QueryError from '../../errors/QueryError'; import QueryError from '../../errors/QueryError';
import { SanitizedCollectionConfig } from '../../collections/config/types'; import { SanitizedCollectionConfig } from '../../collections/config/types';
import { SanitizedGlobalConfig } from '../../globals/config/types'; import { SanitizedGlobalConfig } from '../../globals/config/types';
@@ -8,21 +8,9 @@ import flattenFields from '../../utilities/flattenTopLevelFields';
import { Field, FieldAffectingData } from '../../fields/config/types'; import { Field, FieldAffectingData } from '../../fields/config/types';
import { validateSearchParam } from './validateSearchParams'; import { validateSearchParam } from './validateSearchParams';
import deepCopyObject from '../../utilities/deepCopyObject'; import deepCopyObject from '../../utilities/deepCopyObject';
import { EntityPolicies, validOperators } from './types'; import { EntityPolicies } from './types';
import flattenWhereToOperators from '../flattenWhereToOperators';
const flattenWhere = (query: Where): WhereField[] => Object.entries(query).reduce((flattenedConstraints, [key, val]) => { import { validOperators } from '../../types/constants';
if ((key === 'and' || key === 'or') && Array.isArray(val)) {
return [
...flattenedConstraints,
...val.map((subVal) => flattenWhere(subVal)),
];
}
return [
...flattenedConstraints,
{ [key]: val },
];
}, []);
type Args = { type Args = {
where: Where where: Where
@@ -54,13 +42,13 @@ export async function validateQueryPaths({
const fields = flattenFields(versionFields || (globalConfig || collectionConfig).fields) as FieldAffectingData[]; const fields = flattenFields(versionFields || (globalConfig || collectionConfig).fields) as FieldAffectingData[];
if (typeof where === 'object') { if (typeof where === 'object') {
// const flattenedWhere = flattenWhere(where); // const flattenedWhere = flattenWhere(where);
const whereFields = flattenWhere(where); const whereFields = flattenWhereToOperators(where);
// We need to determine if the whereKey is an AND, OR, or a schema path // We need to determine if the whereKey is an AND, OR, or a schema path
const promises = []; const promises = [];
whereFields.map(async (constraint) => { whereFields.map(async (constraint) => {
Object.keys(constraint).map(async (path) => { Object.keys(constraint).map(async (path) => {
Object.entries(constraint[path]).map(async ([operator, val]) => { Object.entries(constraint[path]).map(async ([operator, val]) => {
if (validOperators.includes(operator)) { if (validOperators.includes(operator as Operator)) {
promises.push(validateSearchParam({ promises.push(validateSearchParam({
collectionConfig: deepCopyObject(collectionConfig), collectionConfig: deepCopyObject(collectionConfig),
globalConfig: deepCopyObject(globalConfig), globalConfig: deepCopyObject(globalConfig),

View File

@@ -3,10 +3,8 @@ import merge from 'deepmerge';
import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types'; import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types';
import { Operation } from '../../../types'; import { Operation } from '../../../types';
import { PayloadRequest, RequestContext } from '../../../express/types'; import { PayloadRequest, RequestContext } from '../../../express/types';
import getValueWithDefault from '../../getDefaultValue';
import { traverseFields } from './traverseFields'; import { traverseFields } from './traverseFields';
import { getExistingRowDoc } from './getExistingRowDoc'; import { getExistingRowDoc } from './getExistingRowDoc';
import { cloneDataFromOriginalDoc } from './cloneDataFromOriginalDoc';
type Args = { type Args = {
data: Record<string, unknown> data: Record<string, unknown>
@@ -28,8 +26,6 @@ type Args = {
// This function is responsible for the following actions, in order: // This function is responsible for the following actions, in order:
// - Run condition // - Run condition
// - Merge original document data into incoming data
// - Compute default values for undefined fields
// - Execute field hooks // - Execute field hooks
// - Validate data // - Validate data
// - Transform data for storage // - Transform data for storage
@@ -59,26 +55,6 @@ export const promise = async ({
const operationLocale = req.locale || defaultLocale; const operationLocale = req.locale || defaultLocale;
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
if (typeof siblingData[field.name] === 'undefined') {
// If no incoming data, but existing document data is found, merge it in
if (typeof siblingDoc[field.name] !== 'undefined') {
if (field.localized && typeof siblingDocWithLocales[field.name] === 'object' && siblingDocWithLocales[field.name] !== null) {
siblingData[field.name] = cloneDataFromOriginalDoc(siblingDocWithLocales[field.name][req.locale]);
} else {
siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name]);
}
// Otherwise compute default value
} else if (typeof field.defaultValue !== 'undefined') {
siblingData[field.name] = await getValueWithDefault({
value: siblingData[field.name],
defaultValue: field.defaultValue,
locale: req.locale,
user: req.user,
});
}
}
// skip validation if the field is localized and the incoming data is null // skip validation if the field is localized and the incoming data is null
if (field.localized && operationLocale !== defaultLocale) { if (field.localized && operationLocale !== defaultLocale) {
if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) { if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) {

View File

@@ -1,6 +1,9 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { PayloadRequest, RequestContext } from '../../../express/types'; import { PayloadRequest, RequestContext } from '../../../express/types';
import { Field, fieldAffectsData, TabAsField, tabHasName, valueIsValueWithRelation } from '../../config/types'; import { Field, fieldAffectsData, TabAsField, tabHasName, valueIsValueWithRelation } from '../../config/types';
import getValueWithDefault from '../../getDefaultValue';
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc';
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc';
import { traverseFields } from './traverseFields'; import { traverseFields } from './traverseFields';
type Args<T> = { type Args<T> = {
@@ -20,6 +23,8 @@ type Args<T> = {
// - Sanitize incoming data // - Sanitize incoming data
// - Execute field hooks // - Execute field hooks
// - Execute field access control // - Execute field access control
// - Merge original document data into incoming data
// - Compute default values for undefined fields
export const promise = async <T>({ export const promise = async <T>({
data, data,
@@ -189,6 +194,22 @@ export const promise = async <T>({
delete siblingData[field.name]; delete siblingData[field.name];
} }
} }
if (typeof siblingData[field.name] === 'undefined') {
// If no incoming data, but existing document data is found, merge it in
if (typeof siblingDoc[field.name] !== 'undefined') {
siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name]);
// Otherwise compute default value
} else if (typeof field.defaultValue !== 'undefined') {
siblingData[field.name] = await getValueWithDefault({
value: siblingData[field.name],
defaultValue: field.defaultValue,
locale: req.locale,
user: req.user,
});
}
}
} }
// Traverse subfields // Traverse subfields
@@ -231,7 +252,7 @@ export const promise = async <T>({
overrideAccess, overrideAccess,
req, req,
siblingData: row, siblingData: row,
siblingDoc: siblingDoc[field.name]?.[i] || {}, siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
context, context,
})); }));
}); });
@@ -258,7 +279,7 @@ export const promise = async <T>({
overrideAccess, overrideAccess,
req, req,
siblingData: row, siblingData: row,
siblingDoc: siblingDoc[field.name]?.[i] || {}, siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
context, context,
})); }));
} }
@@ -291,8 +312,11 @@ export const promise = async <T>({
let tabSiblingData; let tabSiblingData;
let tabSiblingDoc; let tabSiblingDoc;
if (tabHasName(field)) { if (tabHasName(field)) {
tabSiblingData = typeof siblingData[field.name] === 'object' ? siblingData[field.name] : {}; if (typeof siblingData[field.name] !== 'object') siblingData[field.name] = {};
tabSiblingDoc = typeof siblingDoc[field.name] === 'object' ? siblingDoc[field.name] : {}; if (typeof siblingDoc[field.name] !== 'object') siblingDoc[field.name] = {};
tabSiblingData = siblingData[field.name] as Record<string, unknown>;
tabSiblingDoc = siblingDoc[field.name] as Record<string, unknown>;
} else { } else {
tabSiblingData = siblingData; tabSiblingData = siblingData;
tabSiblingDoc = siblingDoc; tabSiblingDoc = siblingDoc;

View File

@@ -15,13 +15,20 @@ import formatName from '../utilities/formatName';
import { withOperators } from './withOperators'; import { withOperators } from './withOperators';
import fieldToSchemaMap from './fieldToWhereInputSchemaMap'; import fieldToSchemaMap from './fieldToWhereInputSchemaMap';
// buildWhereInputType is similar to buildObjectType and operates /** This does as the function name suggests. It builds a where GraphQL input type
// on a field basis with a few distinct differences. * for all the fields which are passed to the function.
// * Each field has different operators which may be valid for a where input type.
// 1. Everything needs to be a GraphQLInputObjectType or scalar / enum * For example, a text field may have a "contains" operator, but a number field
// 2. Relationships, groups, repeaters and flex content are not * may not.
// directly searchable. Instead, we need to build a chained pathname *
// using dot notation so MongoDB can properly search nested paths. * buildWhereInputType is similar to buildObjectType and operates
* on a field basis with a few distinct differences.
*
* 1. Everything needs to be a GraphQLInputObjectType or scalar / enum
* 2. Relationships, groups, repeaters and flex content are not
* directly searchable. Instead, we need to build a chained pathname
* using dot notation so MongoDB can properly search nested paths.
*/
const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => { const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => {
// This is the function that builds nested paths for all // This is the function that builds nested paths for all
// field types with nested paths. // field types with nested paths.

View File

@@ -4,6 +4,7 @@ const operators = {
contains: ['in', 'not_in', 'all'], contains: ['in', 'not_in', 'all'],
comparison: ['greater_than_equal', 'greater_than', 'less_than_equal', 'less_than'], comparison: ['greater_than_equal', 'greater_than', 'less_than_equal', 'less_than'],
geo: ['near'], geo: ['near'],
geojson: ['within', 'intersects'],
}; };
export default operators; export default operators;

View File

@@ -9,124 +9,211 @@ import operators from './operators';
type staticTypes = 'number' | 'text' | 'email' | 'textarea' | 'richText' | 'json' | 'code' | 'checkbox' | 'date' | 'upload' | 'point' | 'relationship' type staticTypes = 'number' | 'text' | 'email' | 'textarea' | 'richText' | 'json' | 'code' | 'checkbox' | 'date' | 'upload' | 'point' | 'relationship'
type dynamicTypes = 'radio' | 'select'
const GeoJSONObject = new GraphQLInputObjectType({
name: 'GeoJSONObject',
fields: {
type: { type: GraphQLString },
coordinates: {
type: GraphQLJSON,
},
},
});
type DefaultsType = { type DefaultsType = {
[key in staticTypes]: { [key in staticTypes]: {
type: GraphQLType | ((field: FieldAffectingData, parentName: string) => GraphQLType); operators: {
operators: string[]; name: string;
type: GraphQLType | ((field: FieldAffectingData, parentName: string) => GraphQLType);
}[];
} }
} & { } & {
radio: { [key in dynamicTypes]: {
type: (field: FieldAffectingData, parentName: string) => GraphQLType; operators: {
operators: string[]; name: string;
} type: ((field: FieldAffectingData, parentName: string) => GraphQLType);
select: { }[];
type: (field: FieldAffectingData, parentName: string) => GraphQLType;
operators: string[];
} }
} }
const defaults: DefaultsType = { const defaults: DefaultsType = {
number: { number: {
type: (field: NumberField): GraphQLType => { operators: [
return field?.name === 'id' ? GraphQLInt : GraphQLFloat; ...[...operators.equality, ...operators.comparison].map((operator) => ({
}, name: operator,
operators: [...operators.equality, ...operators.comparison], type: (field: NumberField): GraphQLType => {
return field?.name === 'id' ? GraphQLInt : GraphQLFloat;
},
})),
],
}, },
text: { text: {
type: GraphQLString, operators: [
operators: [...operators.equality, ...operators.partial, ...operators.contains], ...[...operators.equality, ...operators.partial, ...operators.contains].map((operator) => ({
name: operator,
type: GraphQLString,
})),
],
}, },
email: { email: {
type: EmailAddressResolver, operators: [
operators: [...operators.equality, ...operators.partial, ...operators.contains], ...[...operators.equality, ...operators.partial, ...operators.contains].map((operator) => ({
name: operator,
type: EmailAddressResolver,
})),
],
}, },
textarea: { textarea: {
type: GraphQLString, operators: [
operators: [...operators.equality, ...operators.partial], ...[...operators.equality, ...operators.partial].map((operator) => ({
name: operator,
type: GraphQLString,
})),
],
}, },
richText: { richText: {
type: GraphQLJSON, operators: [
operators: [...operators.equality, ...operators.partial], ...[...operators.equality, ...operators.partial].map((operator) => ({
name: operator,
type: GraphQLJSON,
})),
],
}, },
json: { json: {
type: GraphQLJSON, operators: [
operators: [...operators.equality, ...operators.partial], ...[...operators.equality, ...operators.partial, ...operators.geojson].map((operator) => ({
name: operator,
type: GraphQLJSON,
})),
],
}, },
code: { code: {
type: GraphQLString, operators: [
operators: [...operators.equality, ...operators.partial], ...[...operators.equality, ...operators.partial].map((operator) => ({
name: operator,
type: GraphQLString,
})),
],
}, },
radio: { radio: {
type: (field: RadioField, parentName): GraphQLType => new GraphQLEnumType({ operators: [
name: `${combineParentName(parentName, field.name)}_Input`, ...[...operators.equality, ...operators.partial].map((operator) => ({
values: field.options.reduce((values, option) => { name: operator,
if (optionIsObject(option)) { type: (field: RadioField, parentName): GraphQLType => new GraphQLEnumType({
return { name: `${combineParentName(parentName, field.name)}_Input`,
...values, values: field.options.reduce((values, option) => {
[formatName(option.value)]: { if (optionIsObject(option)) {
value: option.value, return {
}, ...values,
}; [formatName(option.value)]: {
} value: option.value,
},
};
}
return { return {
...values, ...values,
[formatName(option)]: { [formatName(option)]: {
value: option, value: option,
}, },
}; };
}, {}), }, {}),
}), }),
operators: [...operators.equality, ...operators.contains], })),
],
}, },
date: { date: {
type: DateTimeResolver, operators: [
operators: [...operators.equality, ...operators.comparison, 'like'], ...[...operators.equality, ...operators.comparison, 'like'].map((operator) => ({
name: operator,
type: DateTimeResolver,
})),
],
}, },
point: { point: {
type: new GraphQLList(GraphQLFloat), operators: [
operators: [...operators.equality, ...operators.comparison, ...operators.geo], ...[...operators.equality, ...operators.comparison, ...operators.geo].map((operator) => ({
name: operator,
type: new GraphQLList(GraphQLFloat),
})),
...operators.geojson.map((operator) => ({
name: operator,
/**
* @example:
* within: {
* type: "Polygon",
* coordinates: [[
* [0.0, 0.0],
* [1.0, 1.0],
* [1.0, 0.0],
* [0.0, 0.0],
* ]],
* }
* @example
* intersects: {
* type: "Point",
* coordinates: [ 0.5, 0.5 ]
* }
*/
type: GeoJSONObject,
})),
],
}, },
relationship: { relationship: {
type: (field: RelationshipField): GraphQLType => { operators: [
return field?.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString; ...[...operators.equality, ...operators.contains].map((operator) => ({
}, name: operator,
operators: [...operators.equality, ...operators.contains], type: GraphQLString,
})),
],
}, },
upload: { upload: {
type: GraphQLString, operators: [
operators: [...operators.equality], ...operators.equality.map((operator) => ({
name: operator,
type: GraphQLString,
})),
],
}, },
checkbox: { checkbox: {
type: GraphQLBoolean, operators: [
operators: [...operators.equality], ...operators.equality.map((operator) => ({
name: operator,
type: GraphQLBoolean,
})),
],
}, },
select: { select: {
type: (field: SelectField, parentName): GraphQLType => new GraphQLEnumType({ operators: [
name: `${combineParentName(parentName, field.name)}_Input`, ...[...operators.equality, ...operators.contains].map((operator) => ({
values: field.options.reduce((values, option) => { name: operator,
if (typeof option === 'object' && option.value) { type: (field: SelectField, parentName): GraphQLType => new GraphQLEnumType({
return { name: `${combineParentName(parentName, field.name)}_Input`,
...values, values: field.options.reduce((values, option) => {
[formatName(option.value)]: { if (typeof option === 'object' && option.value) {
value: option.value, return {
}, ...values,
}; [formatName(option.value)]: {
} value: option.value,
},
};
}
if (typeof option === 'string') { if (typeof option === 'string') {
return { return {
...values, ...values,
[option]: { [option]: {
value: option, value: option,
}, },
}; };
} }
return values; return values;
}, {}), }, {}),
}), }),
operators: [...operators.equality, ...operators.contains], })),
],
}, },
// array: n/a // array: n/a
// group: n/a // group: n/a
@@ -137,32 +224,64 @@ const defaults: DefaultsType = {
const listOperators = ['in', 'not_in', 'all']; const listOperators = ['in', 'not_in', 'all'];
const gqlTypeCache: Record<string, GraphQLType> = {};
/**
* In GraphQL, you can use "where" as an argument to filter a collection. Example:
* { Posts(where: { title: { equals: "Hello" } }) { text } }
* This function defines the operators for a field's condition in the "where" argument of the collection (it thus gets called for every field).
* For example, in the example above, it would control that
* - "equals" is a valid operator for the "title" field
* - the accepted type of the "equals" argument has to be a string.
*
* @param field the field for which their valid operators inside a "where" argument is being defined
* @param parentName the name of the parent field (if any)
* @returns all the operators (including their types) which can be used as a condition for a given field inside a where
*/
export const withOperators = (field: FieldAffectingData, parentName: string): GraphQLInputObjectType => { export const withOperators = (field: FieldAffectingData, parentName: string): GraphQLInputObjectType => {
if (!defaults?.[field.type]) throw new Error(`Error: ${field.type} has no defaults configured.`); if (!defaults?.[field.type]) throw new Error(`Error: ${field.type} has no defaults configured.`);
const name = `${combineParentName(parentName, field.name)}_operator`; const name = `${combineParentName(parentName, field.name)}_operator`;
// Get the default operators for the field type which are hard-coded above
const fieldOperators = [...defaults[field.type].operators]; const fieldOperators = [...defaults[field.type].operators];
if (!('required' in field) || !field.required) fieldOperators.push('exists');
const initialGqlType: GraphQLType = typeof defaults[field.type].type === 'function' if (!('required' in field) || !field.required) {
? defaults[field.type].type(field, parentName) fieldOperators.push({
: defaults?.[field.type].type; name: 'exists',
type: fieldOperators[0].type,
});
}
return new GraphQLInputObjectType({ return new GraphQLInputObjectType({
name, name,
fields: fieldOperators.reduce((objectTypeFields, operator) => { fields: fieldOperators.reduce((objectTypeFields, operator) => {
let gqlType = initialGqlType; // Get the type of the operator. It can be either static, or dynamic (=> a function)
let gqlType: GraphQLType = typeof operator.type === 'function'
? operator.type(field, parentName)
: operator.type;
if (listOperators.includes(operator)) { // GraphQL does not allow types with duplicate names, so we use this cache to avoid that.
// Without this, select and radio fields would have the same name, and GraphQL would throw an error
// This usually only happens if a custom type is returned from the operator.type function
if (typeof operator.type === 'function' && 'name' in gqlType) {
if (gqlTypeCache[gqlType.name]) {
gqlType = gqlTypeCache[gqlType.name];
} else {
gqlTypeCache[gqlType.name] = gqlType;
}
}
if (listOperators.includes(operator.name)) {
gqlType = new GraphQLList(gqlType); gqlType = new GraphQLList(gqlType);
} else if (operator === 'exists') { } else if (operator.name === 'exists') {
gqlType = GraphQLBoolean; gqlType = GraphQLBoolean;
} }
return { return {
...objectTypeFields, ...objectTypeFields,
[operator]: { [operator.name]: {
type: gqlType, type: gqlType,
}, },
}; };

View File

@@ -63,8 +63,8 @@
"deletingFile": "حدث خطأ أثناء حذف الملف.", "deletingFile": "حدث خطأ أثناء حذف الملف.",
"deletingTitle": "حدث خطأ أثناء حذف {{title}}. يرجى التحقق من الاتصال الخاص بك والمحاولة مرة أخرى.", "deletingTitle": "حدث خطأ أثناء حذف {{title}}. يرجى التحقق من الاتصال الخاص بك والمحاولة مرة أخرى.",
"emailOrPasswordIncorrect": "البريد الإلكتروني أو كلمة المرور المقدمة غير صحيحة.", "emailOrPasswordIncorrect": "البريد الإلكتروني أو كلمة المرور المقدمة غير صحيحة.",
"followingFieldsInvalid_other": "الحقول التالية غير صالحة:",
"followingFieldsInvalid_one": "الحقل التالي غير صالح:", "followingFieldsInvalid_one": "الحقل التالي غير صالح:",
"followingFieldsInvalid_other": "الحقول التالية غير صالحة:",
"incorrectCollection": "مجموعة غير صحيحة", "incorrectCollection": "مجموعة غير صحيحة",
"invalidFileType": "نوع ملف غير صالح", "invalidFileType": "نوع ملف غير صالح",
"invalidFileTypeValue": "نوع ملف غير صالح: {{value}}", "invalidFileTypeValue": "نوع ملف غير صالح: {{value}}",
@@ -173,27 +173,28 @@
"deletedSuccessfully": "تمّ الحذف بنجاح.", "deletedSuccessfully": "تمّ الحذف بنجاح.",
"deleting": "يتمّ الحذف...", "deleting": "يتمّ الحذف...",
"descending": "تنازلي", "descending": "تنازلي",
"duplicate": "تكرار", "deselectAllRows": "إلغاء تحديد جميع الصفوف",
"duplicateWithoutSaving": "تكرار بدون حفظ التّغييرات", "duplicate": "استنساخ",
"duplicateWithoutSaving": "استنساخ بدون حفظ التغييرات",
"edit": "تعديل", "edit": "تعديل",
"editLabel": "تعديل {{label}}", "editLabel": "تعديل {{label}}",
"editing": "جاري التعديل", "editing": "جاري التعديل",
"editingLabel_many": "تعديل {{count}} {{label}}", "editingLabel_many": "تعديل {{count}} {{label}}",
"editingLabel_one": "تعديل {{count}} {{label}}", "editingLabel_one": "تعديل {{count}} {{label}}",
"editingLabel_other": "تعديل {{count}} {{label}}", "editingLabel_other": "تعديل {{count}} {{label}}",
"error": "خطأ",
"errors": "أخطاء",
"email": "البريد الإلكتروني", "email": "البريد الإلكتروني",
"emailAddress": "عنوان البريد الإلكتروني", "emailAddress": "عنوان البريد الإلكتروني",
"enterAValue": "أدخل قيمة", "enterAValue": "أدخل قيمة",
"fallbackToDefaultLocale": "يتمّ استخدام اللّغة الافتراضيّة", "error": "خطأ",
"filter": "فلتر", "errors": "أخطاء",
"filterWhere": "فلتر {{label}} أينما", "fallbackToDefaultLocale": "الرجوع إلى اللغة الافتراضية",
"filters": "فلاتر", "filter": "تصفية",
"globals": "المجموعات العامّة", "filterWhere": "تصفية {{label}} حيث",
"language": "اللّغة", "filters": "عوامل التصفية",
"lastModified": "آخر تعديل في", "globals": "عامة",
"leaveAnyway": "المغادرة على أيّة حال", "language": "اللغة",
"lastModified": "آخر تعديل",
"leaveAnyway": "المغادرة على أي حال",
"leaveWithoutSaving": "المغادرة بدون حفظ", "leaveWithoutSaving": "المغادرة بدون حفظ",
"light": "فاتح", "light": "فاتح",
"loading": "يتمّ التّحميل", "loading": "يتمّ التّحميل",
@@ -201,13 +202,14 @@
"moveDown": "التّحريك إلى الأسفل", "moveDown": "التّحريك إلى الأسفل",
"moveUp": "التّحريك إلى الأعلى", "moveUp": "التّحريك إلى الأعلى",
"newPassword": "كلمة مرور جديدة", "newPassword": "كلمة مرور جديدة",
"noFiltersSet": "لم يتمّ تحديد فلتر", "noFiltersSet": "لم يتم تعيين أي عوامل تصفية",
"noLabel": "<لا يوجد {{label}}>", "noLabel": "<لا {{label}}>",
"noResults": م يتمّ العثور على {{label}}. إمّا أنّه لا يوجد {{label}} حتّى الآن أو أنّه لا يتطابق أيّ منها مع الفلاتر التّي حدّدتها أعلاه.", "noOptions": ا خيارات",
"noValue": "لا توجد قيمة", "noResults": "لا يوجد {{label}}. إما أن لا {{label}} موجودة حتى الآن أو لا تتطابق مع عوامل التصفية التي حددتها أعلاه.",
"none": "None", "noValue": "لا يوجد قيمة",
"notFound": "غير معثور عليه", "none": "لا شيء",
"nothingFound": "لم يتمّ العثور على شيء", "notFound": "غير موجود",
"nothingFound": "لم يتم العثور على شيء",
"of": "من", "of": "من",
"or": "أو", "or": "أو",
"order": "التّرتيب", "order": "التّرتيب",
@@ -219,13 +221,16 @@
"row": "سطر", "row": "سطر",
"rows": "أسطُر", "rows": "أسطُر",
"save": "حفظ", "save": "حفظ",
"saving": تمّ الحفظ...", "saving": "جاري الحفظ...",
"searchBy": "البحث بواسطة {{label}}", "searchBy": "البحث عن طريق {{label}}",
"selectAll": "اختر الكلّ {{count}} {{label}}", "selectAll": "تحديد كل {{count}} {{label}}",
"selectValue": "اختر قيمة", "selectAllRows": "حدد جميع الصفوف",
"selectedCount": "{{count}} {{label}} تمّ اختيارها", "selectValue": "اختيار قيمة",
"sorryNotFound": "عذرًا - ليس هناك ما يتوافق مع طلبك.", "selectedCount": "تم تحديد {{count}} {{label}}",
"showAllLabel": "عرض كل {{label}}",
"sorryNotFound": "عذرًا - لا يوجد شيء يتوافق مع طلبك.",
"sort": "ترتيب", "sort": "ترتيب",
"sortByLabelDirection": "رتّب حسب {{label}} {{direction}}",
"stayOnThisPage": "البقاء على هذه الصفحة", "stayOnThisPage": "البقاء على هذه الصفحة",
"submissionSuccessful": "تمت الإرسال بنجاح.", "submissionSuccessful": "تمت الإرسال بنجاح.",
"submit": "إرسال", "submit": "إرسال",
@@ -282,6 +287,7 @@
"invalidSelection": "هذا الحقل لديه اختيار غير صالح.", "invalidSelection": "هذا الحقل لديه اختيار غير صالح.",
"invalidSelections": "هذا الحقل لديه الاختيارات الغير صالحة التالية:", "invalidSelections": "هذا الحقل لديه الاختيارات الغير صالحة التالية:",
"lessThanMin": "{{value}} أقل من الحد الأدنى المسموح به {{label}} الذي يبلغ {{min}}.", "lessThanMin": "{{value}} أقل من الحد الأدنى المسموح به {{label}} الذي يبلغ {{min}}.",
"limitReached": "تم الوصول إلى الحد الأقصى، يمكن إضافة {{max}} عناصر فقط.",
"longerThanMin": "يجب أن يكون هذا القيمة أطول من الحد الأدنى للطول الذي هو {{minLength}} أحرف.", "longerThanMin": "يجب أن يكون هذا القيمة أطول من الحد الأدنى للطول الذي هو {{minLength}} أحرف.",
"notValidDate": "\"{{value}}\" ليس تاريخا صالحا.", "notValidDate": "\"{{value}}\" ليس تاريخا صالحا.",
"required": "هذا الحقل مطلوب.", "required": "هذا الحقل مطلوب.",

View File

@@ -65,6 +65,7 @@
"emailOrPasswordIncorrect": "Təqdim olunan e-poçt və ya şifrə yanlışdır.", "emailOrPasswordIncorrect": "Təqdim olunan e-poçt və ya şifrə yanlışdır.",
"followingFieldsInvalid_many": "Aşağıdakı sahələr yanlışdır:", "followingFieldsInvalid_many": "Aşağıdakı sahələr yanlışdır:",
"followingFieldsInvalid_one": "Aşağıdakı sahə yanlışdır:", "followingFieldsInvalid_one": "Aşağıdakı sahə yanlışdır:",
"followingFieldsInvalid_other": "Aşağıdaki sahələr yanlışdır:",
"incorrectCollection": "Yanlış Kolleksiya", "incorrectCollection": "Yanlış Kolleksiya",
"invalidFileType": "Yanlış fayl növü", "invalidFileType": "Yanlış fayl növü",
"invalidFileTypeValue": "Yanlış fayl növü: {{value}}", "invalidFileTypeValue": "Yanlış fayl növü: {{value}}",
@@ -173,6 +174,7 @@
"deletedSuccessfully": "Uğurla silindi.", "deletedSuccessfully": "Uğurla silindi.",
"deleting": "Silinir...", "deleting": "Silinir...",
"descending": "Azalan", "descending": "Azalan",
"deselectAllRows": "Bütün sıraları seçimi ləğv edin",
"duplicate": "Dublikat", "duplicate": "Dublikat",
"duplicateWithoutSaving": "Dəyişiklikləri saxlamadan dublikatla", "duplicateWithoutSaving": "Dəyişiklikləri saxlamadan dublikatla",
"edit": "Redaktə et", "edit": "Redaktə et",
@@ -203,6 +205,7 @@
"newPassword": "Yeni şifrə", "newPassword": "Yeni şifrə",
"noFiltersSet": "Filter təyin edilməyib", "noFiltersSet": "Filter təyin edilməyib",
"noLabel": "<Heç bir {{label}}>", "noLabel": "<Heç bir {{label}}>",
"noOptions": "Heç bir seçim yoxdur",
"noResults": "Heç bir {{label}} tapılmadı. Ya hələ {{label}} yoxdur, ya da yuxarıda göstərdiyiniz filtrlərə uyğun gəlmir.", "noResults": "Heç bir {{label}} tapılmadı. Ya hələ {{label}} yoxdur, ya da yuxarıda göstərdiyiniz filtrlərə uyğun gəlmir.",
"noValue": "Dəyər yoxdur", "noValue": "Dəyər yoxdur",
"none": "Heç bir", "none": "Heç bir",
@@ -222,10 +225,13 @@
"saving": "Saxlanılır...", "saving": "Saxlanılır...",
"searchBy": "{{label}} ilə axtar", "searchBy": "{{label}} ilə axtar",
"selectAll": "Bütün {{count}} {{label}} seç", "selectAll": "Bütün {{count}} {{label}} seç",
"selectAllRows": "Bütün sıraları seçin",
"selectValue": "Dəyər seçin", "selectValue": "Dəyər seçin",
"selectedCount": "{{count}} {{label}} seçildi", "selectedCount": "{{count}} {{label}} seçildi",
"showAllLabel": "Bütün {{label}}-ı göstər",
"sorryNotFound": "Üzr istəyirik - sizin tələbinizə uyğun heç nə yoxdur.", "sorryNotFound": "Üzr istəyirik - sizin tələbinizə uyğun heç nə yoxdur.",
"sort": "Sırala", "sort": "Sırala",
"sortByLabelDirection": "{{label}} {{direction}} ilə sırala",
"stayOnThisPage": "Bu səhifədə qal", "stayOnThisPage": "Bu səhifədə qal",
"submissionSuccessful": "Təqdimat uğurlu oldu.", "submissionSuccessful": "Təqdimat uğurlu oldu.",
"submit": "Təqdim et", "submit": "Təqdim et",
@@ -282,6 +288,7 @@
"invalidSelection": "Bu sahədə yanlış seçim edilmişdir.", "invalidSelection": "Bu sahədə yanlış seçim edilmişdir.",
"invalidSelections": "Bu sahədə aşağıdakı yanlış seçimlər edilmişdir:", "invalidSelections": "Bu sahədə aşağıdakı yanlış seçimlər edilmişdir:",
"lessThanMin": "{{value}} icazə verilən minimal {{label}} olan {{min}}-dən kiçikdir.", "lessThanMin": "{{value}} icazə verilən minimal {{label}} olan {{min}}-dən kiçikdir.",
"limitReached": "Limitə çatdınız, yalnız {{max}} element əlavə edilə bilər.",
"longerThanMin": "Bu dəyər {{minLength}} simvoldan uzun olmalıdır.", "longerThanMin": "Bu dəyər {{minLength}} simvoldan uzun olmalıdır.",
"notValidDate": "\"{{value}}\" doğru tarix deyil.", "notValidDate": "\"{{value}}\" doğru tarix deyil.",
"required": "Bu sahə mütləq doldurulmalıdır.", "required": "Bu sahə mütləq doldurulmalıdır.",
@@ -346,4 +353,4 @@
"viewingVersions": "{{entityLabel}} {{documentTitle}} üçün versiyaları göstərir", "viewingVersions": "{{entityLabel}} {{documentTitle}} üçün versiyaları göstərir",
"viewingVersionsGlobal": "Qlobal {{entityLabel}} üçün versiyaları göstərir" "viewingVersionsGlobal": "Qlobal {{entityLabel}} üçün versiyaları göstərir"
} }
} }

View File

@@ -63,8 +63,8 @@
"deletingFile": "Имаше грешка при изтриването на файла.", "deletingFile": "Имаше грешка при изтриването на файла.",
"deletingTitle": "Имаше проблем при изтриването на {{title}}. Моля провери връзката си и опитай отново.", "deletingTitle": "Имаше проблем при изтриването на {{title}}. Моля провери връзката си и опитай отново.",
"emailOrPasswordIncorrect": "Имейлът или паролата не са правилни.", "emailOrPasswordIncorrect": "Имейлът или паролата не са правилни.",
"followingFieldsInvalid_other": "Следните полета са некоректни:",
"followingFieldsInvalid_one": "Следното поле е некоректно:", "followingFieldsInvalid_one": "Следното поле е некоректно:",
"followingFieldsInvalid_other": "Следните полета са некоректни:",
"incorrectCollection": "Некоректно събиране", "incorrectCollection": "Некоректно събиране",
"invalidFileType": "Невалиден тип на файл", "invalidFileType": "Невалиден тип на файл",
"invalidFileTypeValue": "Невалиден тип на файл: {{value}}", "invalidFileTypeValue": "Невалиден тип на файл: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Изтрито успешно.", "deletedSuccessfully": "Изтрито успешно.",
"deleting": "Изтриване...", "deleting": "Изтриване...",
"descending": "Низходящо", "descending": "Низходящо",
"deselectAllRows": "Деселектирайте всички редове",
"duplicate": "Дупликирай", "duplicate": "Дупликирай",
"duplicateWithoutSaving": "Дупликирай без да запазваш промените", "duplicateWithoutSaving": "Дупликирай без да запазваш промените",
"edit": "Редактирай", "edit": "Редактирай",
@@ -203,6 +204,7 @@
"newPassword": "Нова парола", "newPassword": "Нова парола",
"noFiltersSet": "Няма зададени филтри", "noFiltersSet": "Няма зададени филтри",
"noLabel": "<Няма {{label}}>", "noLabel": "<Няма {{label}}>",
"noOptions": "Няма опции",
"noResults": "{{label}} не е открит. {{label}} не съществува или никой не отговаря на зададените филтри.", "noResults": "{{label}} не е открит. {{label}} не съществува или никой не отговаря на зададените филтри.",
"noValue": "Няма стойност", "noValue": "Няма стойност",
"none": "Никакъв", "none": "Никакъв",
@@ -222,10 +224,13 @@
"saving": "Запазване...", "saving": "Запазване...",
"searchBy": "Търси по {{label}}", "searchBy": "Търси по {{label}}",
"selectAll": "Избери всички {{count}} {{label}}", "selectAll": "Избери всички {{count}} {{label}}",
"selectAllRows": "Изберете всички редове",
"selectValue": "Избери стойност", "selectValue": "Избери стойност",
"selectedCount": "{{count}} {{label}} избрани", "selectedCount": "{{count}} {{label}} избрани",
"showAllLabel": "Покажи всички {{label}}",
"sorryNotFound": "Съжаляваме-няма нищо, което да отговаря на търсенето ти.", "sorryNotFound": "Съжаляваме-няма нищо, което да отговаря на търсенето ти.",
"sort": "Сортирай", "sort": "Сортирай",
"sortByLabelDirection": "Сортирай по {{label}} {{direction}}",
"stayOnThisPage": "Остани на тази страница", "stayOnThisPage": "Остани на тази страница",
"submissionSuccessful": "Успешно подаване.", "submissionSuccessful": "Успешно подаване.",
"submit": "Подай", "submit": "Подай",
@@ -261,6 +266,7 @@
"near": "близко" "near": "близко"
}, },
"upload": { "upload": {
"dragAndDrop": "Дръпни и пусни файл",
"dragAndDropHere": "или дръпни и пусни файла тук", "dragAndDropHere": "или дръпни и пусни файла тук",
"fileName": "Име на файла", "fileName": "Име на файла",
"fileSize": "Големина на файла", "fileSize": "Големина на файла",
@@ -269,7 +275,6 @@
"moreInfo": "Повече информация", "moreInfo": "Повече информация",
"selectCollectionToBrowse": "Избери колекция, която да разгледаш", "selectCollectionToBrowse": "Избери колекция, която да разгледаш",
"selectFile": "Избери файл", "selectFile": "Избери файл",
"dragAndDrop": "Дръпни и пусни файл",
"sizes": "Големини", "sizes": "Големини",
"width": "Ширина" "width": "Ширина"
}, },
@@ -282,6 +287,7 @@
"invalidSelection": "Това поле има невалидна селекция.", "invalidSelection": "Това поле има невалидна селекция.",
"invalidSelections": "Това поле има следните невалидни селекции:", "invalidSelections": "Това поле има следните невалидни селекции:",
"lessThanMin": "{{value}} е по-малко от минимално допустимото {{label}} от {{min}}.", "lessThanMin": "{{value}} е по-малко от минимално допустимото {{label}} от {{min}}.",
"limitReached": "Достигнат е лимитът, могат да бъдат добавени само {{max}} елемента.",
"longerThanMin": "Тази стойност трябва да е по-голяма от минималната стойност от {{minLength}} символа.", "longerThanMin": "Тази стойност трябва да е по-голяма от минималната стойност от {{minLength}} символа.",
"notValidDate": "\"{{value}}\" не е валидна дата.", "notValidDate": "\"{{value}}\" не е валидна дата.",
"required": "Това поле е задължително.", "required": "Това поле е задължително.",
@@ -327,8 +333,8 @@
"saveDraft": "Запази чернова", "saveDraft": "Запази чернова",
"selectLocales": "Избери локализации за показване", "selectLocales": "Избери локализации за показване",
"selectVersionToCompare": "Избери версия за сравняване", "selectVersionToCompare": "Избери версия за сравняване",
"showingVersionsFor": "Показване на версии за:",
"showLocales": "Покажи преводи:", "showLocales": "Покажи преводи:",
"showingVersionsFor": "Показване на версии за:",
"status": "Статус", "status": "Статус",
"type": "Тип", "type": "Тип",
"unpublish": "Скрий", "unpublish": "Скрий",

View File

@@ -63,8 +63,8 @@
"deletingFile": "Při mazání souboru došlo k chybě.", "deletingFile": "Při mazání souboru došlo k chybě.",
"deletingTitle": "Při mazání {{title}} došlo k chybě. Zkontrolujte své připojení a zkuste to znovu.", "deletingTitle": "Při mazání {{title}} došlo k chybě. Zkontrolujte své připojení a zkuste to znovu.",
"emailOrPasswordIncorrect": "Zadaný email nebo heslo není správné.", "emailOrPasswordIncorrect": "Zadaný email nebo heslo není správné.",
"followingFieldsInvalid_other": "Následující pole jsou neplatná:",
"followingFieldsInvalid_one": "Následující pole je neplatné:", "followingFieldsInvalid_one": "Následující pole je neplatné:",
"followingFieldsInvalid_other": "Následující pole jsou neplatná:",
"incorrectCollection": "Nesprávná kolekce", "incorrectCollection": "Nesprávná kolekce",
"invalidFileType": "Neplatný typ souboru", "invalidFileType": "Neplatný typ souboru",
"invalidFileTypeValue": "Neplatný typ souboru: {{value}}", "invalidFileTypeValue": "Neplatný typ souboru: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Úspěšně odstraněno.", "deletedSuccessfully": "Úspěšně odstraněno.",
"deleting": "Odstraňování...", "deleting": "Odstraňování...",
"descending": "Sestupně", "descending": "Sestupně",
"deselectAllRows": "Zrušte výběr všech řádků",
"duplicate": "Duplikovat", "duplicate": "Duplikovat",
"duplicateWithoutSaving": "Duplikovat bez uložení změn", "duplicateWithoutSaving": "Duplikovat bez uložení změn",
"edit": "Upravit", "edit": "Upravit",
@@ -203,6 +204,7 @@
"newPassword": "Nové heslo", "newPassword": "Nové heslo",
"noFiltersSet": "Nenastaveny žádné filtry", "noFiltersSet": "Nenastaveny žádné filtry",
"noLabel": "<Žádný {{label}}>", "noLabel": "<Žádný {{label}}>",
"noOptions": "Žádné možnosti",
"noResults": "Nebyly nalezeny žádné {{label}}. Buď ještě neexistují žádné {{label}}, nebo žádné nesplňují filtry, které jste zadali výše.", "noResults": "Nebyly nalezeny žádné {{label}}. Buď ještě neexistují žádné {{label}}, nebo žádné nesplňují filtry, které jste zadali výše.",
"noValue": "Žádná hodnota", "noValue": "Žádná hodnota",
"none": "Žádné", "none": "Žádné",
@@ -222,10 +224,13 @@
"saving": "Ukládání...", "saving": "Ukládání...",
"searchBy": "Vyhledat podle {{label}}", "searchBy": "Vyhledat podle {{label}}",
"selectAll": "Vybrat vše {{count}} {{label}}", "selectAll": "Vybrat vše {{count}} {{label}}",
"selectAllRows": "Vyberte všechny řádky",
"selectValue": "Vyberte hodnotu", "selectValue": "Vyberte hodnotu",
"selectedCount": "Vybráno {{count}} {{label}}", "selectedCount": "Vybráno {{count}} {{label}}",
"showAllLabel": "Zobrazit všechny {{label}}",
"sorryNotFound": "Je nám líto, ale neexistuje nic, co by odpovídalo vašemu požadavku.", "sorryNotFound": "Je nám líto, ale neexistuje nic, co by odpovídalo vašemu požadavku.",
"sort": "Třídit", "sort": "Třídit",
"sortByLabelDirection": "Seřadit podle {{label}} {{direction}}",
"stayOnThisPage": "Zůstat na této stránce", "stayOnThisPage": "Zůstat na této stránce",
"submissionSuccessful": "Odeslání úspěšné.", "submissionSuccessful": "Odeslání úspěšné.",
"submit": "Odeslat", "submit": "Odeslat",
@@ -247,20 +252,21 @@
"welcome": "Vítejte" "welcome": "Vítejte"
}, },
"operators": { "operators": {
"contains": "obsahuje",
"equals": "rovná se", "equals": "rovná se",
"isNotEqualTo": "není rovno",
"isIn": "je v",
"isNotIn": "není in",
"exists": "existuje", "exists": "existuje",
"isGreaterThan": "je větší než", "isGreaterThan": "je větší než",
"isGreaterThanOrEqualTo": "je větší nebo rovno",
"isIn": "je v",
"isLessThan": "je menší než", "isLessThan": "je menší než",
"isLessThanOrEqualTo": "je menší nebo rovno", "isLessThanOrEqualTo": "je menší nebo rovno",
"isGreaterThanOrEqualTo": "je větší nebo rovno",
"near": "blízko",
"isLike": "je jako", "isLike": "je jako",
"contains": "obsahuje" "isNotEqualTo": "není rovno",
"isNotIn": "není in",
"near": "blízko"
}, },
"upload": { "upload": {
"dragAndDrop": "Přetáhněte soubor",
"dragAndDropHere": "nebo sem přetáhněte soubor", "dragAndDropHere": "nebo sem přetáhněte soubor",
"fileName": "Název souboru", "fileName": "Název souboru",
"fileSize": "Velikost souboru", "fileSize": "Velikost souboru",
@@ -269,7 +275,6 @@
"moreInfo": "Více informací", "moreInfo": "Více informací",
"selectCollectionToBrowse": "Vyberte kolekci pro procházení", "selectCollectionToBrowse": "Vyberte kolekci pro procházení",
"selectFile": "Vyberte soubor", "selectFile": "Vyberte soubor",
"dragAndDrop": "Přetáhněte soubor",
"sizes": "Velikosti", "sizes": "Velikosti",
"width": "Šířka" "width": "Šířka"
}, },
@@ -282,6 +287,7 @@
"invalidSelection": "Toto pole má neplatný výběr.", "invalidSelection": "Toto pole má neplatný výběr.",
"invalidSelections": "Toto pole má následující neplatné výběry:", "invalidSelections": "Toto pole má následující neplatné výběry:",
"lessThanMin": "{{value}} je nižší než minimálně povolená {{label}} {{min}}.", "lessThanMin": "{{value}} je nižší než minimálně povolená {{label}} {{min}}.",
"limitReached": "Dosáhnutý limit, mohou být přidány pouze {{max}} položky.",
"longerThanMin": "Tato hodnota musí být delší než minimální délka {{minLength}} znaků.", "longerThanMin": "Tato hodnota musí být delší než minimální délka {{minLength}} znaků.",
"notValidDate": "\"{{value}}\" není platné datum.", "notValidDate": "\"{{value}}\" není platné datum.",
"required": "Toto pole je povinné.", "required": "Toto pole je povinné.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "Beim Löschen der Datei ist ein Fehler aufgetreten.", "deletingFile": "Beim Löschen der Datei ist ein Fehler aufgetreten.",
"deletingTitle": "Es gab ein Problem während der Löschung von {{title}}. Bitte überprüfe deine Verbindung und versuche es erneut.", "deletingTitle": "Es gab ein Problem während der Löschung von {{title}}. Bitte überprüfe deine Verbindung und versuche es erneut.",
"emailOrPasswordIncorrect": "Die E-Mail-Adresse oder das Passwort sind nicht korrekt.", "emailOrPasswordIncorrect": "Die E-Mail-Adresse oder das Passwort sind nicht korrekt.",
"followingFieldsInvalid_other": "Die folgenden Felder sind nicht korrekt:",
"followingFieldsInvalid_one": "Das folgende Feld ist nicht korrekt:", "followingFieldsInvalid_one": "Das folgende Feld ist nicht korrekt:",
"followingFieldsInvalid_other": "Die folgenden Felder sind nicht korrekt:",
"incorrectCollection": "Falsche Sammlung", "incorrectCollection": "Falsche Sammlung",
"invalidFileType": "Ungültiger Datei-Typ", "invalidFileType": "Ungültiger Datei-Typ",
"invalidFileTypeValue": "Ungültiger Datei-Typ: {{value}}", "invalidFileTypeValue": "Ungültiger Datei-Typ: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Erfolgreich gelöscht.", "deletedSuccessfully": "Erfolgreich gelöscht.",
"deleting": "Lösche...", "deleting": "Lösche...",
"descending": "Absteigend", "descending": "Absteigend",
"deselectAllRows": "Alle Zeilen abwählen",
"duplicate": "Duplizieren", "duplicate": "Duplizieren",
"duplicateWithoutSaving": "Dupliziere ohne Änderungen zu speichern", "duplicateWithoutSaving": "Dupliziere ohne Änderungen zu speichern",
"edit": "Bearbeiten", "edit": "Bearbeiten",
@@ -184,9 +185,9 @@
"email": "E-Mail", "email": "E-Mail",
"emailAddress": "E-Mail-Adresse", "emailAddress": "E-Mail-Adresse",
"enterAValue": "Gib einen Wert ein", "enterAValue": "Gib einen Wert ein",
"fallbackToDefaultLocale": "Rückgriff auf das Standardgebietsschema",
"error": "Fehler", "error": "Fehler",
"errors": "Fehler", "errors": "Fehler",
"fallbackToDefaultLocale": "Rückgriff auf das Standardgebietsschema",
"filter": "Filter", "filter": "Filter",
"filterWhere": "Filter {{label}} wo", "filterWhere": "Filter {{label}} wo",
"filters": "Filter", "filters": "Filter",
@@ -203,6 +204,7 @@
"newPassword": "Neues Passwort", "newPassword": "Neues Passwort",
"noFiltersSet": "Keine Filter gesetzt", "noFiltersSet": "Keine Filter gesetzt",
"noLabel": "<Kein {{label}}>", "noLabel": "<Kein {{label}}>",
"noOptions": "Keine Optionen",
"noResults": "Keine {{label}} gefunden. Entweder es existieren keine {{label}} oder es gibt keine Übereinstimmung zu den von dir verwendeten Filtern.", "noResults": "Keine {{label}} gefunden. Entweder es existieren keine {{label}} oder es gibt keine Übereinstimmung zu den von dir verwendeten Filtern.",
"noValue": "Kein Wert", "noValue": "Kein Wert",
"none": "Kein", "none": "Kein",
@@ -222,10 +224,13 @@
"saving": "Speichert...", "saving": "Speichert...",
"searchBy": "Suche nach {{label}}", "searchBy": "Suche nach {{label}}",
"selectAll": "Alle auswählen {{count}} {{label}}", "selectAll": "Alle auswählen {{count}} {{label}}",
"selectAllRows": "Wählen Sie alle Zeilen aus",
"selectValue": "Wert auswählen", "selectValue": "Wert auswählen",
"selectedCount": "{{count}} {{label}} ausgewählt", "selectedCount": "{{count}} {{label}} ausgewählt",
"showAllLabel": "Zeige alle {{label}}",
"sorryNotFound": "Entschuldige, es entspricht nichts deiner Anfrage", "sorryNotFound": "Entschuldige, es entspricht nichts deiner Anfrage",
"sort": "Sortieren", "sort": "Sortieren",
"sortByLabelDirection": "Sortieren nach {{label}} {{direction}}",
"stayOnThisPage": "Auf dieser Seite bleiben", "stayOnThisPage": "Auf dieser Seite bleiben",
"submissionSuccessful": "Einrichung erfolgreich.", "submissionSuccessful": "Einrichung erfolgreich.",
"submit": "Senden", "submit": "Senden",
@@ -282,6 +287,7 @@
"invalidSelection": "Dieses Feld hat eine inkorrekte Auswahl.", "invalidSelection": "Dieses Feld hat eine inkorrekte Auswahl.",
"invalidSelections": "'Dieses Feld enthält die folgenden inkorrekten Auswahlen:'", "invalidSelections": "'Dieses Feld enthält die folgenden inkorrekten Auswahlen:'",
"lessThanMin": "{{value}} ist kleiner als der minimal erlaubte {{label}} von {{min}}.", "lessThanMin": "{{value}} ist kleiner als der minimal erlaubte {{label}} von {{min}}.",
"limitReached": "Limit erreicht, es können nur {{max}} Elemente hinzugefügt werden.",
"longerThanMin": "Dieser Wert muss länger als die minimale Länge von {{minLength}} Zeichen sein.", "longerThanMin": "Dieser Wert muss länger als die minimale Länge von {{minLength}} Zeichen sein.",
"notValidDate": "\"{{value}}\" ist kein gültiges Datum.", "notValidDate": "\"{{value}}\" ist kein gültiges Datum.",
"required": "Pflichtfeld", "required": "Pflichtfeld",

View File

@@ -173,6 +173,7 @@
"deletedSuccessfully": "Deleted successfully.", "deletedSuccessfully": "Deleted successfully.",
"deleting": "Deleting...", "deleting": "Deleting...",
"descending": "Descending", "descending": "Descending",
"deselectAllRows": "Deselect all rows",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"duplicateWithoutSaving": "Duplicate without saving changes", "duplicateWithoutSaving": "Duplicate without saving changes",
"edit": "Edit", "edit": "Edit",
@@ -203,6 +204,7 @@
"newPassword": "New Password", "newPassword": "New Password",
"noFiltersSet": "No filters set", "noFiltersSet": "No filters set",
"noLabel": "<No {{label}}>", "noLabel": "<No {{label}}>",
"noOptions": "No options",
"noResults": "No {{label}} found. Either no {{label}} exist yet or none match the filters you've specified above.", "noResults": "No {{label}} found. Either no {{label}} exist yet or none match the filters you've specified above.",
"noValue": "No value", "noValue": "No value",
"none": "None", "none": "None",
@@ -222,10 +224,13 @@
"saving": "Saving...", "saving": "Saving...",
"searchBy": "Search by {{label}}", "searchBy": "Search by {{label}}",
"selectAll": "Select all {{count}} {{label}}", "selectAll": "Select all {{count}} {{label}}",
"selectAllRows": "Select all rows",
"selectValue": "Select a value", "selectValue": "Select a value",
"selectedCount": "{{count}} {{label}} selected", "selectedCount": "{{count}} {{label}} selected",
"showAllLabel": "Show all {{label}}",
"sorryNotFound": "Sorry—there is nothing to correspond with your request.", "sorryNotFound": "Sorry—there is nothing to correspond with your request.",
"sort": "Sort", "sort": "Sort",
"sortByLabelDirection": "Sort by {{label}} {{direction}}",
"stayOnThisPage": "Stay on this page", "stayOnThisPage": "Stay on this page",
"submissionSuccessful": "Submission Successful.", "submissionSuccessful": "Submission Successful.",
"submit": "Submit", "submit": "Submit",
@@ -276,6 +281,7 @@
"validation": { "validation": {
"emailAddress": "Please enter a valid email address.", "emailAddress": "Please enter a valid email address.",
"enterNumber": "Please enter a valid number.", "enterNumber": "Please enter a valid number.",
"limitReached": "Limit reached, only {{max}} items can be added.",
"fieldHasNo": "This field has no {{label}}", "fieldHasNo": "This field has no {{label}}",
"greaterThanMax": "{{value}} is greater than the max allowed {{label}} of {{max}}.", "greaterThanMax": "{{value}} is greater than the max allowed {{label}} of {{max}}.",
"invalidInput": "This field has an invalid input.", "invalidInput": "This field has an invalid input.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "Ocurrió un error al eliminar el archivo.", "deletingFile": "Ocurrió un error al eliminar el archivo.",
"deletingTitle": "Ocurrió un error al eliminar {{title}}. Por favor revisa tu conexión y vuelve a intentarlo.", "deletingTitle": "Ocurrió un error al eliminar {{title}}. Por favor revisa tu conexión y vuelve a intentarlo.",
"emailOrPasswordIncorrect": "El correo o la contraseña introducida es incorrecta.", "emailOrPasswordIncorrect": "El correo o la contraseña introducida es incorrecta.",
"followingFieldsInvalid_other": "Los siguientes campos son inválidos:",
"followingFieldsInvalid_one": "El siguiente campo es inválido:", "followingFieldsInvalid_one": "El siguiente campo es inválido:",
"followingFieldsInvalid_other": "Los siguientes campos son inválidos:",
"incorrectCollection": "Colección Incorrecta", "incorrectCollection": "Colección Incorrecta",
"invalidFileType": "Tipo de archivo inválido", "invalidFileType": "Tipo de archivo inválido",
"invalidFileTypeValue": "Tipo de archivo inválido: {{value}}", "invalidFileTypeValue": "Tipo de archivo inválido: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Borrado exitosamente.", "deletedSuccessfully": "Borrado exitosamente.",
"deleting": "Eliminando...", "deleting": "Eliminando...",
"descending": "Descendente", "descending": "Descendente",
"deselectAllRows": "Deselecciona todas las filas",
"duplicate": "Duplicar", "duplicate": "Duplicar",
"duplicateWithoutSaving": "Duplicar sin guardar cambios", "duplicateWithoutSaving": "Duplicar sin guardar cambios",
"edit": "Editar", "edit": "Editar",
@@ -203,6 +204,7 @@
"newPassword": "Nueva contraseña", "newPassword": "Nueva contraseña",
"noFiltersSet": "No hay filtros establecidos", "noFiltersSet": "No hay filtros establecidos",
"noLabel": "<Sin {{label}}>", "noLabel": "<Sin {{label}}>",
"noOptions": "Sin opciones",
"noResults": "No encontramos {{label}}. Puede que no existan {{label}} todavía o no hay coincidencias con los filtros introducidos arriba.", "noResults": "No encontramos {{label}}. Puede que no existan {{label}} todavía o no hay coincidencias con los filtros introducidos arriba.",
"noValue": "Sin valor", "noValue": "Sin valor",
"none": "Ninguna", "none": "Ninguna",
@@ -222,10 +224,13 @@
"saving": "Guardando...", "saving": "Guardando...",
"searchBy": "Buscar por {{label}}", "searchBy": "Buscar por {{label}}",
"selectAll": "Seleccionar todo {{count}} {{label}}", "selectAll": "Seleccionar todo {{count}} {{label}}",
"selectAllRows": "Selecciona todas las filas",
"selectValue": "Selecciona un valor", "selectValue": "Selecciona un valor",
"selectedCount": "{{count}} {{label}} seleccionado", "selectedCount": "{{count}} {{label}} seleccionado",
"showAllLabel": "Muestra todas {{label}}",
"sorryNotFound": "Lo sentimos. No hay nada que corresponda con tu solicitud.", "sorryNotFound": "Lo sentimos. No hay nada que corresponda con tu solicitud.",
"sort": "Ordenar", "sort": "Ordenar",
"sortByLabelDirection": "Ordenar por {{label}} {{direction}}",
"stayOnThisPage": "Permanecer en esta página", "stayOnThisPage": "Permanecer en esta página",
"submissionSuccessful": "Envío realizado correctamente.", "submissionSuccessful": "Envío realizado correctamente.",
"submit": "Enviar", "submit": "Enviar",
@@ -282,6 +287,7 @@
"invalidSelection": "La selección en este campo es inválida.", "invalidSelection": "La selección en este campo es inválida.",
"invalidSelections": "Este campo tiene las siguientes selecciones inválidas:", "invalidSelections": "Este campo tiene las siguientes selecciones inválidas:",
"lessThanMin": "{{value}} es menor que el {{label}} mínimo permitido de {{min}}.", "lessThanMin": "{{value}} es menor que el {{label}} mínimo permitido de {{min}}.",
"limitReached": "Se ha alcanzado el límite, solo se pueden agregar {{max}} elementos.",
"longerThanMin": "Este dato debe ser más largo que el mínimo de {{minLength}} caracteres.", "longerThanMin": "Este dato debe ser más largo que el mínimo de {{minLength}} caracteres.",
"notValidDate": "\"{{value}}\" es una fecha inválida.", "notValidDate": "\"{{value}}\" es una fecha inválida.",
"required": "Este campo es obligatorio.", "required": "Este campo es obligatorio.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "هنگام حذف فایل خطایی روی داد.", "deletingFile": "هنگام حذف فایل خطایی روی داد.",
"deletingTitle": "هنگام حذف {{title}} خطایی رخ داد. لطفاً وضعیت اتصال اینترنت خود را بررسی کنید.", "deletingTitle": "هنگام حذف {{title}} خطایی رخ داد. لطفاً وضعیت اتصال اینترنت خود را بررسی کنید.",
"emailOrPasswordIncorrect": "رایانامه یا گذرواژه ارائه شده نادرست است.", "emailOrPasswordIncorrect": "رایانامه یا گذرواژه ارائه شده نادرست است.",
"followingFieldsInvalid_other": "کادرهای زیر نامعتبر هستند:",
"followingFieldsInvalid_one": "کادر زیر نامعتبر است:", "followingFieldsInvalid_one": "کادر زیر نامعتبر است:",
"followingFieldsInvalid_other": "کادرهای زیر نامعتبر هستند:",
"incorrectCollection": "مجموعه نادرست", "incorrectCollection": "مجموعه نادرست",
"invalidFileType": "نوع رسانه نامعتبر است", "invalidFileType": "نوع رسانه نامعتبر است",
"invalidFileTypeValue": "نوع رسانه نامعتبر: {{value}}", "invalidFileTypeValue": "نوع رسانه نامعتبر: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "با موفقیت حذف شد.", "deletedSuccessfully": "با موفقیت حذف شد.",
"deleting": "در حال حذف...", "deleting": "در حال حذف...",
"descending": "رو به پایین", "descending": "رو به پایین",
"deselectAllRows": "تمام سطرها را از انتخاب خارج کنید",
"duplicate": "تکراری", "duplicate": "تکراری",
"duplicateWithoutSaving": "رونوشت بدون ذخیره کردن تغییرات", "duplicateWithoutSaving": "رونوشت بدون ذخیره کردن تغییرات",
"edit": "نگارش", "edit": "نگارش",
@@ -203,6 +204,7 @@
"newPassword": "گذرواژه تازه", "newPassword": "گذرواژه تازه",
"noFiltersSet": "هیچ علامت‌گذاری تنظیم نشده", "noFiltersSet": "هیچ علامت‌گذاری تنظیم نشده",
"noLabel": "<No {{label}}>", "noLabel": "<No {{label}}>",
"noOptions": "بدون گزینه",
"noResults": "هیچ {{label}} یافت نشد. {{label}} یا هنوز وجود ندارد یا هیچ کدام با علامت‌گذاری‌هایی که در بالا مشخص کرده اید مطابقت ندارد.", "noResults": "هیچ {{label}} یافت نشد. {{label}} یا هنوز وجود ندارد یا هیچ کدام با علامت‌گذاری‌هایی که در بالا مشخص کرده اید مطابقت ندارد.",
"noValue": "بدون مقدار", "noValue": "بدون مقدار",
"none": "هیچ یک", "none": "هیچ یک",
@@ -222,10 +224,13 @@
"saving": "در حال ذخیره...", "saving": "در حال ذخیره...",
"searchBy": "جستجو بر اساس {{label}}", "searchBy": "جستجو بر اساس {{label}}",
"selectAll": "انتخاب همه {{count}} {{label}}", "selectAll": "انتخاب همه {{count}} {{label}}",
"selectAllRows": "انتخاب تمام سطرها",
"selectValue": "یک مقدار را انتخاب کنید", "selectValue": "یک مقدار را انتخاب کنید",
"selectedCount": "{{count}} {{label}} انتخاب شد", "selectedCount": "{{count}} {{label}} انتخاب شد",
"showAllLabel": "نمایش همه {{label}}",
"sorryNotFound": "متأسفانه چیزی برای مطابقت با درخواست شما وجود ندارد.", "sorryNotFound": "متأسفانه چیزی برای مطابقت با درخواست شما وجود ندارد.",
"sort": "مرتب‌سازی", "sort": "مرتب‌سازی",
"sortByLabelDirection": "مرتب کردن بر اساس {{label}} {{direction}}",
"stayOnThisPage": "ماندن در این برگه", "stayOnThisPage": "ماندن در این برگه",
"submissionSuccessful": "با موفقیت ثبت شد.", "submissionSuccessful": "با موفقیت ثبت شد.",
"submit": "فرستادن", "submit": "فرستادن",
@@ -282,6 +287,7 @@
"invalidSelection": "این کادر دارای یک انتخاب نامعتبر است.", "invalidSelection": "این کادر دارای یک انتخاب نامعتبر است.",
"invalidSelections": "این کادر دارای انتخاب‌های نامعتبر زیر است:", "invalidSelections": "این کادر دارای انتخاب‌های نامعتبر زیر است:",
"lessThanMin": "{{value}} کمتر از حداقل مجاز برای {{label}} است که {{min}} است.", "lessThanMin": "{{value}} کمتر از حداقل مجاز برای {{label}} است که {{min}} است.",
"limitReached": "محدودیت رسیده است، فقط {{max}} مورد می تواند اضافه شود.",
"longerThanMin": "ورودی باید بیش از حداقل {{minLength}} واژه باشد.", "longerThanMin": "ورودی باید بیش از حداقل {{minLength}} واژه باشد.",
"notValidDate": "\"{{value}}\" یک تاریخ معتبر نیست.", "notValidDate": "\"{{value}}\" یک تاریخ معتبر نیست.",
"required": "این کادر اجباری است.", "required": "این کادر اجباری است.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "Une erreur s'est produite lors de la suppression du fichier.", "deletingFile": "Une erreur s'est produite lors de la suppression du fichier.",
"deletingTitle": "Une erreur s'est produite lors de la suppression de {{title}}. Veuillez vérifier votre connexion puis réessayer.", "deletingTitle": "Une erreur s'est produite lors de la suppression de {{title}}. Veuillez vérifier votre connexion puis réessayer.",
"emailOrPasswordIncorrect": "L'adresse e-mail ou le mot de passe fourni est incorrect.", "emailOrPasswordIncorrect": "L'adresse e-mail ou le mot de passe fourni est incorrect.",
"followingFieldsInvalid_other": "Les champs suivants ne sont pas valides :",
"followingFieldsInvalid_one": "Le champ suivant n'est pas valide :", "followingFieldsInvalid_one": "Le champ suivant n'est pas valide :",
"followingFieldsInvalid_other": "Les champs suivants ne sont pas valides :",
"incorrectCollection": "Collection incorrecte", "incorrectCollection": "Collection incorrecte",
"invalidFileType": "Type de fichier invalide", "invalidFileType": "Type de fichier invalide",
"invalidFileTypeValue": "Type de fichier invalide : {{value}}", "invalidFileTypeValue": "Type de fichier invalide : {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Supprimé(e) avec succès.", "deletedSuccessfully": "Supprimé(e) avec succès.",
"deleting": "Suppression en cours...", "deleting": "Suppression en cours...",
"descending": "Descendant(e)", "descending": "Descendant(e)",
"deselectAllRows": "Désélectionner toutes les lignes",
"duplicate": "Dupliquer", "duplicate": "Dupliquer",
"duplicateWithoutSaving": "Dupliquer sans enregistrer les modifications", "duplicateWithoutSaving": "Dupliquer sans enregistrer les modifications",
"edit": "Éditer", "edit": "Éditer",
@@ -203,6 +204,7 @@
"newPassword": "Nouveau mot de passe", "newPassword": "Nouveau mot de passe",
"noFiltersSet": "Aucun filtre défini", "noFiltersSet": "Aucun filtre défini",
"noLabel": "<Pas de {{label}}>", "noLabel": "<Pas de {{label}}>",
"noOptions": "Aucune option",
"noResults": "Aucun(e) {{label}} trouvé(e). Soit aucun(e) {{label}} n'existe encore, soit aucun(e) ne correspond aux filtres que vous avez spécifiés ci-dessus", "noResults": "Aucun(e) {{label}} trouvé(e). Soit aucun(e) {{label}} n'existe encore, soit aucun(e) ne correspond aux filtres que vous avez spécifiés ci-dessus",
"noValue": "Aucune valeur", "noValue": "Aucune valeur",
"none": "Aucun(e)", "none": "Aucun(e)",
@@ -222,10 +224,13 @@
"saving": "Sauvegarde en cours...", "saving": "Sauvegarde en cours...",
"searchBy": "Rechercher par {{label}}", "searchBy": "Rechercher par {{label}}",
"selectAll": "Tout sélectionner {{count}} {{label}}", "selectAll": "Tout sélectionner {{count}} {{label}}",
"selectAllRows": "Sélectionnez toutes les lignes",
"selectValue": "Sélectionnez une valeur", "selectValue": "Sélectionnez une valeur",
"selectedCount": "{{count}} {{label}} sélectionné", "selectedCount": "{{count}} {{label}} sélectionné",
"showAllLabel": "Afficher tous les {{label}}",
"sorryNotFound": "Désolé, rien ne correspond à votre demande.", "sorryNotFound": "Désolé, rien ne correspond à votre demande.",
"sort": "Trier", "sort": "Trier",
"sortByLabelDirection": "Trier par {{label}} {{direction}}",
"stayOnThisPage": "Rester sur cette page", "stayOnThisPage": "Rester sur cette page",
"submissionSuccessful": "Soumission réussie.", "submissionSuccessful": "Soumission réussie.",
"submit": "Soumettre", "submit": "Soumettre",
@@ -282,6 +287,7 @@
"invalidSelection": "Ce champ a une sélection invalide.", "invalidSelection": "Ce champ a une sélection invalide.",
"invalidSelections": "Ce champ contient des sélections invalides suivantes :", "invalidSelections": "Ce champ contient des sélections invalides suivantes :",
"lessThanMin": "{{value}} est inférieur au min autorisé {{label}} de {{min}}.", "lessThanMin": "{{value}} est inférieur au min autorisé {{label}} de {{min}}.",
"limitReached": "Limite atteinte, seulement {{max}} éléments peuvent être ajoutés.",
"longerThanMin": "Cette valeur doit être supérieure à la longueur minimale de {{minLength}} caractères.", "longerThanMin": "Cette valeur doit être supérieure à la longueur minimale de {{minLength}} caractères.",
"notValidDate": "\"{{value}}\" n'est pas une date valide.", "notValidDate": "\"{{value}}\" n'est pas une date valide.",
"required": "Ce champ est requis.", "required": "Ce champ est requis.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "Dogodila se pogreška pri brisanju datoteke.", "deletingFile": "Dogodila se pogreška pri brisanju datoteke.",
"deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molim provjerite svoju internetsku vezu i pokušajte ponovno.", "deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molim provjerite svoju internetsku vezu i pokušajte ponovno.",
"emailOrPasswordIncorrect": "Email ili lozinka netočni.", "emailOrPasswordIncorrect": "Email ili lozinka netočni.",
"followingFieldsInvalid_other": "Ova polja su nevaljana:",
"followingFieldsInvalid_one": " Ovo polje je nevaljano:", "followingFieldsInvalid_one": " Ovo polje je nevaljano:",
"followingFieldsInvalid_other": "Ova polja su nevaljana:",
"incorrectCollection": "Nevaljana kolekcija", "incorrectCollection": "Nevaljana kolekcija",
"invalidFileType": "Nevaljan tip datoteke", "invalidFileType": "Nevaljan tip datoteke",
"invalidFileTypeValue": "Nevaljan tip datoteke: {{value}}", "invalidFileTypeValue": "Nevaljan tip datoteke: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Uspješno obrisano.", "deletedSuccessfully": "Uspješno obrisano.",
"deleting": "Brisanje...", "deleting": "Brisanje...",
"descending": "Silazno", "descending": "Silazno",
"deselectAllRows": "Odznači sve redove",
"duplicate": "Duplikat", "duplicate": "Duplikat",
"duplicateWithoutSaving": "Dupliciraj bez spremanja promjena", "duplicateWithoutSaving": "Dupliciraj bez spremanja promjena",
"edit": "Uredi", "edit": "Uredi",
@@ -203,6 +204,7 @@
"newPassword": "Nova lozinka", "newPassword": "Nova lozinka",
"noFiltersSet": "Nema postavljenih filtera", "noFiltersSet": "Nema postavljenih filtera",
"noLabel": "<Nema {{label}}>", "noLabel": "<Nema {{label}}>",
"noOptions": "Nema opcija",
"noResults": "Nema pronađenih {{label}}. Ili {{label}} još uvijek ne postoji ili nijedan od odgovara postavljenim filterima.", "noResults": "Nema pronađenih {{label}}. Ili {{label}} još uvijek ne postoji ili nijedan od odgovara postavljenim filterima.",
"noValue": "Bez vrijednosti", "noValue": "Bez vrijednosti",
"none": "Nijedan", "none": "Nijedan",
@@ -222,10 +224,13 @@
"saving": "Spremanje...", "saving": "Spremanje...",
"searchBy": "Traži po {{label}}", "searchBy": "Traži po {{label}}",
"selectAll": "Odaberite sve {{count}} {{label}}", "selectAll": "Odaberite sve {{count}} {{label}}",
"selectAllRows": "Odaberite sve redove",
"selectValue": "Odaberi vrijednost", "selectValue": "Odaberi vrijednost",
"selectedCount": "{{count}} {{label}} odabrano", "selectedCount": "{{count}} {{label}} odabrano",
"showAllLabel": "Prikaži sve {{label}}",
"sorryNotFound": "Nažalost, ne postoji ništa što odgovara vašem zahtjevu.", "sorryNotFound": "Nažalost, ne postoji ništa što odgovara vašem zahtjevu.",
"sort": "Sortiraj", "sort": "Sortiraj",
"sortByLabelDirection": "Sortiraj prema {{label}} {{direction}}",
"stayOnThisPage": "Ostani na ovoj stranici", "stayOnThisPage": "Ostani na ovoj stranici",
"submissionSuccessful": "Uspješno slanje", "submissionSuccessful": "Uspješno slanje",
"submit": "Podnesi", "submit": "Podnesi",
@@ -282,6 +287,7 @@
"invalidSelection": "Ovo polje ima nevaljan odabir.", "invalidSelection": "Ovo polje ima nevaljan odabir.",
"invalidSelections": "Ovo polje ima sljedeće nevaljane odabire:", "invalidSelections": "Ovo polje ima sljedeće nevaljane odabire:",
"lessThanMin": "{{value}} is below the minimum allowable {{label}} limit of {{min}}.", "lessThanMin": "{{value}} is below the minimum allowable {{label}} limit of {{min}}.",
"limitReached": "Dosegnut je limit, može se dodati samo {{max}} stavki.",
"longerThanMin": "Ova vrijednost mora biti duža od minimalne dužine od {{minLength}} znakova", "longerThanMin": "Ova vrijednost mora biti duža od minimalne dužine od {{minLength}} znakova",
"notValidDate": "\"{{value}}\" nije valjan datum.", "notValidDate": "\"{{value}}\" nije valjan datum.",
"required": "Ovo polje je obvezno.", "required": "Ovo polje je obvezno.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "Hiba történt a fájl törlésekor.", "deletingFile": "Hiba történt a fájl törlésekor.",
"deletingTitle": "Hiba történt a {{title}} törlése közben. Kérjük, ellenőrizze a kapcsolatot, és próbálja meg újra.", "deletingTitle": "Hiba történt a {{title}} törlése közben. Kérjük, ellenőrizze a kapcsolatot, és próbálja meg újra.",
"emailOrPasswordIncorrect": "A megadott e-mail-cím vagy jelszó helytelen.", "emailOrPasswordIncorrect": "A megadott e-mail-cím vagy jelszó helytelen.",
"followingFieldsInvalid_other": "A következő mezők érvénytelenek:",
"followingFieldsInvalid_one": "A következő mező érvénytelen:", "followingFieldsInvalid_one": "A következő mező érvénytelen:",
"followingFieldsInvalid_other": "A következő mezők érvénytelenek:",
"incorrectCollection": "Helytelen gyűjtemény", "incorrectCollection": "Helytelen gyűjtemény",
"invalidFileType": "Érvénytelen fájltípus", "invalidFileType": "Érvénytelen fájltípus",
"invalidFileTypeValue": "Érvénytelen fájltípus: {{value}}", "invalidFileTypeValue": "Érvénytelen fájltípus: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Sikeresen törölve.", "deletedSuccessfully": "Sikeresen törölve.",
"deleting": "Törlés...", "deleting": "Törlés...",
"descending": "Csökkenő", "descending": "Csökkenő",
"deselectAllRows": "Jelölje ki az összes sort",
"duplicate": "Duplikálás", "duplicate": "Duplikálás",
"duplicateWithoutSaving": "Duplikálás a módosítások mentése nélkül", "duplicateWithoutSaving": "Duplikálás a módosítások mentése nélkül",
"edit": "Szerkesztés", "edit": "Szerkesztés",
@@ -203,6 +204,7 @@
"newPassword": "Új jelszó", "newPassword": "Új jelszó",
"noFiltersSet": "Nincs beállítva szűrő", "noFiltersSet": "Nincs beállítva szűrő",
"noLabel": "<No {{label}}>", "noLabel": "<No {{label}}>",
"noOptions": "Nincs lehetőség",
"noResults": "Nem találtunk {{label}}. Vagy még nem létezik {{label}}, vagy egyik sem felel meg a fent megadott szűrőknek.", "noResults": "Nem találtunk {{label}}. Vagy még nem létezik {{label}}, vagy egyik sem felel meg a fent megadott szűrőknek.",
"noValue": "Nincs érték", "noValue": "Nincs érték",
"none": "Semmi", "none": "Semmi",
@@ -222,10 +224,13 @@
"saving": "Mentés...", "saving": "Mentés...",
"searchBy": "Keresés a következő szerint: {{label}}", "searchBy": "Keresés a következő szerint: {{label}}",
"selectAll": "Az összes kijelölése: {{count}} {{label}}", "selectAll": "Az összes kijelölése: {{count}} {{label}}",
"selectAllRows": "Válassza ki az összes sort",
"selectValue": "Válasszon ki egy értéket", "selectValue": "Válasszon ki egy értéket",
"selectedCount": "{{count}} {{label}} kiválasztva", "selectedCount": "{{count}} {{label}} kiválasztva",
"showAllLabel": "Mutasd az összes {{címke}}",
"sorryNotFound": "Sajnáljuk nincs semmi, ami megfelelne a kérésének.", "sorryNotFound": "Sajnáljuk nincs semmi, ami megfelelne a kérésének.",
"sort": "Rendezés", "sort": "Rendezés",
"sortByLabelDirection": "Rendezés {{label}} {{direction}} szerint",
"stayOnThisPage": "Maradjon ezen az oldalon", "stayOnThisPage": "Maradjon ezen az oldalon",
"submissionSuccessful": "Beküldés sikeres.", "submissionSuccessful": "Beküldés sikeres.",
"submit": "Beküldés", "submit": "Beküldés",
@@ -247,20 +252,21 @@
"welcome": "Üdvözöljük" "welcome": "Üdvözöljük"
}, },
"operators": { "operators": {
"contains": "tartalmaz",
"equals": "egyenlő", "equals": "egyenlő",
"isNotEqualTo": "nem egyenlő",
"isIn": "benne van",
"isNotIn": "nincs benne",
"exists": "létezik", "exists": "létezik",
"isGreaterThan": "nagyobb, mint", "isGreaterThan": "nagyobb, mint",
"isGreaterThanOrEqualTo": "nagyobb vagy egyenlő, mint",
"isIn": "benne van",
"isLessThan": "kisebb, mint", "isLessThan": "kisebb, mint",
"isLessThanOrEqualTo": "kisebb vagy egyenlő, mint", "isLessThanOrEqualTo": "kisebb vagy egyenlő, mint",
"isGreaterThanOrEqualTo": "nagyobb vagy egyenlő, mint",
"near": "közel",
"isLike": "olyan, mint", "isLike": "olyan, mint",
"contains": "tartalmaz" "isNotEqualTo": "nem egyenlő",
"isNotIn": "nincs benne",
"near": "közel"
}, },
"upload": { "upload": {
"dragAndDrop": "Húzzon ide egy fájlt",
"dragAndDropHere": "vagy húzzon ide egy fájlt", "dragAndDropHere": "vagy húzzon ide egy fájlt",
"fileName": "Fájlnév", "fileName": "Fájlnév",
"fileSize": "Fájl mérete", "fileSize": "Fájl mérete",
@@ -269,7 +275,6 @@
"moreInfo": "További információ", "moreInfo": "További információ",
"selectCollectionToBrowse": "Válassza ki a böngészni kívánt gyűjteményt", "selectCollectionToBrowse": "Válassza ki a böngészni kívánt gyűjteményt",
"selectFile": "Válasszon ki egy fájlt", "selectFile": "Válasszon ki egy fájlt",
"dragAndDrop": "Húzzon ide egy fájlt",
"sizes": "Méretek", "sizes": "Méretek",
"width": "Szélesség" "width": "Szélesség"
}, },
@@ -282,6 +287,7 @@
"invalidSelection": "Ez a mező érvénytelen kijelöléssel rendelkezik.", "invalidSelection": "Ez a mező érvénytelen kijelöléssel rendelkezik.",
"invalidSelections": "Ez a mező a következő érvénytelen kijelöléseket tartalmazza:", "invalidSelections": "Ez a mező a következő érvénytelen kijelöléseket tartalmazza:",
"lessThanMin": "{{value}} kisebb, mint a megengedett minimum {{label}} érték, ami {{min}}.", "lessThanMin": "{{value}} kisebb, mint a megengedett minimum {{label}} érték, ami {{min}}.",
"limitReached": "Elérte a korlátot, csak {{max}} elem adható hozzá.",
"longerThanMin": "Ennek az értéknek hosszabbnak kell lennie, mint a minimális {{minLength}} karakter hosszúság.", "longerThanMin": "Ennek az értéknek hosszabbnak kell lennie, mint a minimális {{minLength}} karakter hosszúság.",
"notValidDate": "\" {{value}} \" nem érvényes dátum.", "notValidDate": "\" {{value}} \" nem érvényes dátum.",
"required": "Ez a mező kötelező.", "required": "Ez a mező kötelező.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "Si è verificato un errore durante l'eleminazione del file.", "deletingFile": "Si è verificato un errore durante l'eleminazione del file.",
"deletingTitle": "Si è verificato un errore durante l'eliminazione di {{title}}. Per favore controlla la tua connessione e riprova.", "deletingTitle": "Si è verificato un errore durante l'eliminazione di {{title}}. Per favore controlla la tua connessione e riprova.",
"emailOrPasswordIncorrect": "L'email o la password fornita non è corretta.", "emailOrPasswordIncorrect": "L'email o la password fornita non è corretta.",
"followingFieldsInvalid_other": "I seguenti campi non sono validi:",
"followingFieldsInvalid_one": "Il seguente campo non è valido:", "followingFieldsInvalid_one": "Il seguente campo non è valido:",
"followingFieldsInvalid_other": "I seguenti campi non sono validi:",
"incorrectCollection": "Collezione non corretta", "incorrectCollection": "Collezione non corretta",
"invalidFileType": "Tipo di file non valido", "invalidFileType": "Tipo di file non valido",
"invalidFileTypeValue": "Tipo di file non valido: {{value}}", "invalidFileTypeValue": "Tipo di file non valido: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "Eliminato con successo.", "deletedSuccessfully": "Eliminato con successo.",
"deleting": "Sto eliminando...", "deleting": "Sto eliminando...",
"descending": "Decrescente", "descending": "Decrescente",
"deselectAllRows": "Deseleziona tutte le righe",
"duplicate": "Duplica", "duplicate": "Duplica",
"duplicateWithoutSaving": "Duplica senza salvare le modifiche", "duplicateWithoutSaving": "Duplica senza salvare le modifiche",
"edit": "Modificare", "edit": "Modificare",
@@ -203,6 +204,7 @@
"newPassword": "Nuova Password", "newPassword": "Nuova Password",
"noFiltersSet": "Nessun filtro impostato", "noFiltersSet": "Nessun filtro impostato",
"noLabel": "<No {{label}}>", "noLabel": "<No {{label}}>",
"noOptions": "Nessuna opzione",
"noResults": "Nessun {{label}} trovato. Non esiste ancora nessun {{label}} oppure nessuno corrisponde ai filtri che hai specificato sopra.", "noResults": "Nessun {{label}} trovato. Non esiste ancora nessun {{label}} oppure nessuno corrisponde ai filtri che hai specificato sopra.",
"noValue": "Nessun valore", "noValue": "Nessun valore",
"none": "Nessuno", "none": "Nessuno",
@@ -222,10 +224,13 @@
"saving": "Salvo...", "saving": "Salvo...",
"searchBy": "Cerca per {{label}}", "searchBy": "Cerca per {{label}}",
"selectAll": "Seleziona tutto {{count}} {{label}}", "selectAll": "Seleziona tutto {{count}} {{label}}",
"selectAllRows": "Seleziona tutte le righe",
"selectValue": "Seleziona un valore", "selectValue": "Seleziona un valore",
"selectedCount": "{{count}} {{label}} selezionato", "selectedCount": "{{count}} {{label}} selezionato",
"showAllLabel": "Mostra tutti {{label}}",
"sorryNotFound": "Siamo spiacenti, non c'è nulla che corrisponda alla tua richiesta.", "sorryNotFound": "Siamo spiacenti, non c'è nulla che corrisponda alla tua richiesta.",
"sort": "Ordina", "sort": "Ordina",
"sortByLabelDirection": "Ordina per {{label}} {{direction}}",
"stayOnThisPage": "Rimani su questa pagina", "stayOnThisPage": "Rimani su questa pagina",
"submissionSuccessful": "Invio riuscito.", "submissionSuccessful": "Invio riuscito.",
"submit": "Invia", "submit": "Invia",
@@ -282,6 +287,7 @@
"invalidSelection": "Questo campo ha una selezione non valida.", "invalidSelection": "Questo campo ha una selezione non valida.",
"invalidSelections": "'In questo campo sono presenti le seguenti selezioni non valide:'", "invalidSelections": "'In questo campo sono presenti le seguenti selezioni non valide:'",
"lessThanMin": "{{value}} è inferiore al minimo consentito {{label}} di {{min}}.", "lessThanMin": "{{value}} è inferiore al minimo consentito {{label}} di {{min}}.",
"limitReached": "Raggiunto il limite, possono essere aggiunti solo {{max}} elementi.",
"longerThanMin": "Questo valore deve essere più lungo della lunghezza minima di {{minLength}} caratteri.", "longerThanMin": "Questo valore deve essere più lungo della lunghezza minima di {{minLength}} caratteri.",
"notValidDate": "\"{{value}}\" non è una data valida.", "notValidDate": "\"{{value}}\" non è una data valida.",
"required": "Questo campo è obbligatorio.", "required": "Questo campo è obbligatorio.",

View File

@@ -63,8 +63,8 @@
"deletingFile": "ファイルの削除中にエラーが発生しました。", "deletingFile": "ファイルの削除中にエラーが発生しました。",
"deletingTitle": "{{title}} を削除する際にエラーが発生しました。接続を確認してからもう一度お試しください。", "deletingTitle": "{{title}} を削除する際にエラーが発生しました。接続を確認してからもう一度お試しください。",
"emailOrPasswordIncorrect": "メールアドレス、または、パスワードが正しくありません。", "emailOrPasswordIncorrect": "メールアドレス、または、パスワードが正しくありません。",
"followingFieldsInvalid_other": "次のフィールドは無効です:",
"followingFieldsInvalid_one": "次のフィールドは無効です:", "followingFieldsInvalid_one": "次のフィールドは無効です:",
"followingFieldsInvalid_other": "次のフィールドは無効です:",
"incorrectCollection": "不正なコレクション", "incorrectCollection": "不正なコレクション",
"invalidFileType": "無効なファイル形式", "invalidFileType": "無効なファイル形式",
"invalidFileTypeValue": "無効なファイル形式: {{value}}", "invalidFileTypeValue": "無効なファイル形式: {{value}}",
@@ -173,14 +173,15 @@
"deletedSuccessfully": "正常に削除されました。", "deletedSuccessfully": "正常に削除されました。",
"deleting": "削除しています...", "deleting": "削除しています...",
"descending": "降順", "descending": "降順",
"deselectAllRows": "すべての行の選択を解除します",
"duplicate": "複製", "duplicate": "複製",
"duplicateWithoutSaving": "変更を保存せずに複製", "duplicateWithoutSaving": "変更を保存せずに複製",
"edit": "編集", "edit": "編集",
"editLabel": "{{label}} を編集", "editLabel": "{{label}} を編集",
"editing": "編集",
"editingLabel_many": "{{count}}つの{{label}}を編集しています", "editingLabel_many": "{{count}}つの{{label}}を編集しています",
"editingLabel_one": "{{count}}つの{{label}}を編集しています", "editingLabel_one": "{{count}}つの{{label}}を編集しています",
"editingLabel_other": "{{count}}つの{{label}}を編集しています", "editingLabel_other": "{{count}}つの{{label}}を編集しています",
"editing": "編集",
"email": "メールアドレス", "email": "メールアドレス",
"emailAddress": "メールアドレス", "emailAddress": "メールアドレス",
"enterAValue": "値を入力", "enterAValue": "値を入力",
@@ -203,6 +204,7 @@
"newPassword": "新しいパスワード", "newPassword": "新しいパスワード",
"noFiltersSet": "絞り込みが未設定です。", "noFiltersSet": "絞り込みが未設定です。",
"noLabel": "<No {{label}}>", "noLabel": "<No {{label}}>",
"noOptions": "選択肢なし",
"noResults": "{{label}} データが見つかりませんでした。データが存在しない、または、絞り込みに一致するものがありません。", "noResults": "{{label}} データが見つかりませんでした。データが存在しない、または、絞り込みに一致するものがありません。",
"noValue": "未設定", "noValue": "未設定",
"none": "なし", "none": "なし",
@@ -222,10 +224,13 @@
"saving": "保存しています...", "saving": "保存しています...",
"searchBy": "{{label}} で検索", "searchBy": "{{label}} で検索",
"selectAll": "すべての{{count}}つの{{label}}を選択", "selectAll": "すべての{{count}}つの{{label}}を選択",
"selectAllRows": "すべての行を選択します",
"selectValue": "値を選択", "selectValue": "値を選択",
"selectedCount": "{{count}}つの{{label}}を選択中", "selectedCount": "{{count}}つの{{label}}を選択中",
"showAllLabel": "すべての{{label}}を表示する",
"sorryNotFound": "申し訳ありません。リクエストに対応する内容が見つかりませんでした。", "sorryNotFound": "申し訳ありません。リクエストに対応する内容が見つかりませんでした。",
"sort": "並び替え", "sort": "並び替え",
"sortByLabelDirection": "{{label}}により並べ替え {{direction}}",
"stayOnThisPage": "この画面にとどまる", "stayOnThisPage": "この画面にとどまる",
"submissionSuccessful": "送信が成功しました。", "submissionSuccessful": "送信が成功しました。",
"submit": "送信", "submit": "送信",
@@ -247,20 +252,21 @@
"welcome": "ようこそ" "welcome": "ようこそ"
}, },
"operators": { "operators": {
"contains": "含む",
"equals": "等しい", "equals": "等しい",
"isNotEqualTo": "等しくない",
"isIn": "あります",
"isNotIn": "入っていません",
"exists": "存在す", "exists": "存在す",
"isGreaterThan": "より大きい", "isGreaterThan": "より大きい",
"isGreaterThanOrEqualTo": "以上",
"isIn": "あります",
"isLessThan": "より小さい", "isLessThan": "より小さい",
"isLessThanOrEqualTo": "以下", "isLessThanOrEqualTo": "以下",
"isGreaterThanOrEqualTo": "以上",
"near": "近く",
"isLike": "のような", "isLike": "のような",
"contains": "含む" "isNotEqualTo": "等しくない",
"isNotIn": "入っていません",
"near": "近く"
}, },
"upload": { "upload": {
"dragAndDrop": "ファイルをドラッグ アンド ドロップする",
"dragAndDropHere": "または、このエリアにファイルをドラッグ & ドロップ", "dragAndDropHere": "または、このエリアにファイルをドラッグ & ドロップ",
"fileName": "ファイル名", "fileName": "ファイル名",
"fileSize": "ファイル容量", "fileSize": "ファイル容量",
@@ -269,7 +275,6 @@
"moreInfo": "詳細を表示", "moreInfo": "詳細を表示",
"selectCollectionToBrowse": "閲覧するコレクションを選択", "selectCollectionToBrowse": "閲覧するコレクションを選択",
"selectFile": "ファイルを選択", "selectFile": "ファイルを選択",
"dragAndDrop": "ファイルをドラッグ アンド ドロップする",
"sizes": "容量", "sizes": "容量",
"width": "横幅" "width": "横幅"
}, },
@@ -282,6 +287,7 @@
"invalidSelection": "無効な選択です。", "invalidSelection": "無効な選択です。",
"invalidSelections": "次の無効な選択があります: ", "invalidSelections": "次の無効な選択があります: ",
"lessThanMin": "{{value}}は許容最小{{label}}の{{min}}未満です。", "lessThanMin": "{{value}}は許容最小{{label}}の{{min}}未満です。",
"limitReached": "制限に達しました、{{max}}個以上のアイテムを追加することはできません。",
"longerThanMin": "{{minLength}} 文字以上にする必要があります。", "longerThanMin": "{{minLength}} 文字以上にする必要があります。",
"notValidDate": "\"{{value}}\" は有効な日付ではありません。", "notValidDate": "\"{{value}}\" は有効な日付ではありません。",
"required": "必須フィールドです。", "required": "必須フィールドです。",
@@ -293,15 +299,18 @@
"validUploadID": "有効なアップロードIDではありません。" "validUploadID": "有効なアップロードIDではありません。"
}, },
"version": { "version": {
"aboutToPublishSelection": "選択中のすべての{{label}}を公開しようとしています。よろしいですか?",
"aboutToRestore": "この {{label}} データを {{versionDate}} 時点のバージョンに復元しようとしています。", "aboutToRestore": "この {{label}} データを {{versionDate}} 時点のバージョンに復元しようとしています。",
"aboutToRestoreGlobal": "グローバルな {{label}} データを {{versionDate}} 時点のバージョンに復元しようとしています。", "aboutToRestoreGlobal": "グローバルな {{label}} データを {{versionDate}} 時点のバージョンに復元しようとしています。",
"aboutToRevertToPublished": "このデータの変更を公開時の状態に戻そうとしています。よろしいですか?", "aboutToRevertToPublished": "このデータの変更を公開時の状態に戻そうとしています。よろしいですか?",
"aboutToUnpublish": "このデータを非公開にしようとしています。よろしいですか?", "aboutToUnpublish": "このデータを非公開にしようとしています。よろしいですか?",
"aboutToUnpublishSelection": "選択したすべての{{label}}の公開を取り消そうとしています。よろしいですか?",
"autosave": "自動保存", "autosave": "自動保存",
"autosavedSuccessfully": "自動保存に成功しました。", "autosavedSuccessfully": "自動保存に成功しました。",
"autosavedVersion": "自動保存されたバージョン", "autosavedVersion": "自動保存されたバージョン",
"changed": "変更済み", "changed": "変更済み",
"compareVersion": "バージョンを比較:", "compareVersion": "バージョンを比較:",
"confirmPublish": "公開を確認する",
"confirmRevertToSaved": "保存された状態に戻す確認", "confirmRevertToSaved": "保存された状態に戻す確認",
"confirmUnpublish": "非公開の確認", "confirmUnpublish": "非公開の確認",
"confirmVersionRestoration": "バージョン復元の確認", "confirmVersionRestoration": "バージョン復元の確認",
@@ -313,6 +322,7 @@
"noRowsFound": "{{label}} は未設定です", "noRowsFound": "{{label}} は未設定です",
"preview": "プレビュー", "preview": "プレビュー",
"problemRestoringVersion": "このバージョンの復元に問題がありました。", "problemRestoringVersion": "このバージョンの復元に問題がありました。",
"publish": "公開する",
"publishChanges": "変更内容を公開", "publishChanges": "変更内容を公開",
"published": "公開済み", "published": "公開済み",
"restoreThisVersion": "このバージョンを復元", "restoreThisVersion": "このバージョンを復元",
@@ -324,6 +334,7 @@
"selectLocales": "表示するロケールを選択", "selectLocales": "表示するロケールを選択",
"selectVersionToCompare": "比較するバージョンを選択", "selectVersionToCompare": "比較するバージョンを選択",
"showLocales": "ロケールを表示:", "showLocales": "ロケールを表示:",
"showingVersionsFor": "次のバージョンを表示します:",
"status": "ステータス", "status": "ステータス",
"type": "タイプ", "type": "タイプ",
"unpublish": "非公開", "unpublish": "非公開",
@@ -332,6 +343,7 @@
"versionCount_many": "{{count}} バージョンがあります", "versionCount_many": "{{count}} バージョンがあります",
"versionCount_none": "バージョンがありません", "versionCount_none": "バージョンがありません",
"versionCount_one": "{{count}} バージョンがあります", "versionCount_one": "{{count}} バージョンがあります",
"versionCount_other": "{{count}}バージョンが見つかりました",
"versionCreatedOn": "{{version}} 作成日時:", "versionCreatedOn": "{{version}} 作成日時:",
"versionID": "バージョンID", "versionID": "バージョンID",
"versions": "バージョン", "versions": "バージョン",

View File

@@ -63,8 +63,8 @@
"deletingFile": "ဖိုင်ကိုဖျက်ရာတွင် အမှားအယွင်းရှိနေသည်။", "deletingFile": "ဖိုင်ကိုဖျက်ရာတွင် အမှားအယွင်းရှိနေသည်။",
"deletingTitle": "{{title}} ကို ဖျက်ရာတွင် အမှားအယွင်းရှိခဲ့သည်။ သင့် အင်တာနက်လိုင်းအား စစ်ဆေးပြီး ထပ်မံကြို့စားကြည့်ပါ။", "deletingTitle": "{{title}} ကို ဖျက်ရာတွင် အမှားအယွင်းရှိခဲ့သည်။ သင့် အင်တာနက်လိုင်းအား စစ်ဆေးပြီး ထပ်မံကြို့စားကြည့်ပါ။",
"emailOrPasswordIncorrect": "ထည့်သွင်းထားသော အီးမေးလ် သို့မဟုတ် စကားဝှက်သည် မမှန်ပါ။", "emailOrPasswordIncorrect": "ထည့်သွင်းထားသော အီးမေးလ် သို့မဟုတ် စကားဝှက်သည် မမှန်ပါ။",
"followingFieldsInvalid_other": "ထည့်သွင်းထားသော အချက်အလက်များသည် မမှန်ကန်ပါ။",
"followingFieldsInvalid_one": "ထည့်သွင်းထားသော အချက်အလက်သည် မမှန်ကန်ပါ။", "followingFieldsInvalid_one": "ထည့်သွင်းထားသော အချက်အလက်သည် မမှန်ကန်ပါ။",
"followingFieldsInvalid_other": "ထည့်သွင်းထားသော အချက်အလက်များသည် မမှန်ကန်ပါ။",
"incorrectCollection": "မှားယွင်းသော စုစည်းမှု", "incorrectCollection": "မှားယွင်းသော စုစည်းမှု",
"invalidFileType": "မမှန်ကန်သော ဖိုင်အမျိုးအစား", "invalidFileType": "မမှန်ကန်သော ဖိုင်အမျိုးအစား",
"invalidFileTypeValue": "မမှန်ကန်သော ဖိုင်အမျိုးအစား: {{value}}", "invalidFileTypeValue": "မမှန်ကန်သော ဖိုင်အမျိုးအစား: {{value}}",
@@ -173,6 +173,7 @@
"deletedSuccessfully": "အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။", "deletedSuccessfully": "အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။",
"deleting": "ဖျက်နေဆဲ ...", "deleting": "ဖျက်နေဆဲ ...",
"descending": "ဆင်းသက်လာသည်။", "descending": "ဆင်းသက်လာသည်။",
"deselectAllRows": "အားလုံးကို မရွေးနိုင်ပါ",
"duplicate": "ပုံတူပွားမည်။", "duplicate": "ပုံတူပွားမည်။",
"duplicateWithoutSaving": "သေချာပါပြီ။", "duplicateWithoutSaving": "သေချာပါပြီ။",
"edit": "တည်းဖြတ်ပါ။", "edit": "တည်းဖြတ်ပါ။",
@@ -203,6 +204,7 @@
"newPassword": "စကားဝှက် အသစ်", "newPassword": "စကားဝှက် အသစ်",
"noFiltersSet": "စစ်ထုတ်မှုများ မသတ်မှတ်ထားပါ။", "noFiltersSet": "စစ်ထုတ်မှုများ မသတ်မှတ်ထားပါ။",
"noLabel": "<မရှိ {{label}}>", "noLabel": "<မရှိ {{label}}>",
"noOptions": "ရွေးချယ်မှုမရှိပါ",
"noResults": "{{label}} မတွေ့ပါ။ {{label}} မရှိသေးသည်ဖြစ်စေ အထက်တွင်ဖော်ပြထားသော စစ်ထုတ်မှုများနှင့် ကိုက်ညီမှုမရှိပါ။", "noResults": "{{label}} မတွေ့ပါ။ {{label}} မရှိသေးသည်ဖြစ်စေ အထက်တွင်ဖော်ပြထားသော စစ်ထုတ်မှုများနှင့် ကိုက်ညီမှုမရှိပါ။",
"noValue": "တန်ဖိုး မရှိပါ။", "noValue": "တန်ဖိုး မရှိပါ။",
"none": "တစ်ခုမှ", "none": "တစ်ခုမှ",
@@ -222,10 +224,13 @@
"saving": "သိမ်းနေဆဲ ...", "saving": "သိမ်းနေဆဲ ...",
"searchBy": "ရှာဖွေပါ။", "searchBy": "ရှာဖွေပါ။",
"selectAll": "{{count}} {{label}} အားလုံးကို ရွေးပါ", "selectAll": "{{count}} {{label}} အားလုံးကို ရွေးပါ",
"selectAllRows": "အားလုံးကိုရွေးချယ်ပါ",
"selectValue": "တစ်ခုခုကို ရွေးချယ်ပါ။", "selectValue": "တစ်ခုခုကို ရွေးချယ်ပါ။",
"selectedCount": "{{count}} {{label}} ကို ရွေးထားသည်။", "selectedCount": "{{count}} {{label}} ကို ရွေးထားသည်။",
"showAllLabel": "Tunjukkan semua {{label}}",
"sorryNotFound": "ဝမ်းနည်းပါသည်။ သင်ရှာနေတဲ့ဟာ ဒီမှာမရှိပါ။", "sorryNotFound": "ဝမ်းနည်းပါသည်။ သင်ရှာနေတဲ့ဟာ ဒီမှာမရှိပါ။",
"sort": "အစဉ်လိုက်", "sort": "အစဉ်လိုက်",
"sortByLabelDirection": "အစဉ်အလိုက် စီမံခန့်ခွဲထားသည် {{label}} {{direction}}",
"stayOnThisPage": "ဒီမှာပဲ ဆက်နေမည်။", "stayOnThisPage": "ဒီမှာပဲ ဆက်နေမည်။",
"submissionSuccessful": "သိမ်းဆည်းမှု အောင်မြင်ပါသည်။", "submissionSuccessful": "သိမ်းဆည်းမှု အောင်မြင်ပါသည်။",
"submit": "သိမ်းဆည်းမည်။", "submit": "သိမ်းဆည်းမည်။",
@@ -282,6 +287,7 @@
"invalidSelection": "ဤအကွက်တွင် မမှန်ကန်သော ရွေးချယ်မှုတစ်ခုရှိသည်။", "invalidSelection": "ဤအကွက်တွင် မမှန်ကန်သော ရွေးချယ်မှုတစ်ခုရှိသည်။",
"invalidSelections": "ဤအကွက်တွင် အောက်ပါ မမှန်ကန်သော ရွေးချယ်မှုများ ရှိသည်", "invalidSelections": "ဤအကွက်တွင် အောက်ပါ မမှန်ကန်သော ရွေးချယ်မှုများ ရှိသည်",
"lessThanMin": "{{value}} သည် {{min}} ထက် ပိုမိုနိမ့်သည်။ ဤသည်ဖြင့် {{label}} အနည်းဆုံးခွင့်ပြုထားသော တန်ဖိုးထက် နိမ့်သည်။", "lessThanMin": "{{value}} သည် {{min}} ထက် ပိုမိုနိမ့်သည်။ ဤသည်ဖြင့် {{label}} အနည်းဆုံးခွင့်ပြုထားသော တန်ဖိုးထက် နိမ့်သည်။",
"limitReached": "Had yang dibenarkan telah dicapai, hanya {{max}} item sahaja yang boleh ditambah.",
"longerThanMin": "ဤတန်ဖိုးသည် အနိမ့်ဆုံးအရှည် {{minLength}} စာလုံးထက် ပိုရှည်ရမည်။", "longerThanMin": "ဤတန်ဖိုးသည် အနိမ့်ဆုံးအရှည် {{minLength}} စာလုံးထက် ပိုရှည်ရမည်။",
"notValidDate": "\"{{value}}\" သည် တရားဝင်ရက်စွဲမဟုတ်ပါ။", "notValidDate": "\"{{value}}\" သည် တရားဝင်ရက်စွဲမဟုတ်ပါ။",
"required": "ဤအကွက်ကို လိုအပ်သည်။", "required": "ဤအကွက်ကို လိုအပ်သည်။",

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