Compare commits
13 Commits
db-postgre
...
v1.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5f2332755 | ||
|
|
0acd7b8706 | ||
|
|
d91b44cbb3 | ||
|
|
e03a8e6b03 | ||
|
|
846485388a | ||
|
|
8d83e05948 | ||
|
|
7963d04a27 | ||
|
|
20b6b29c79 | ||
|
|
fdfdfc83f3 | ||
|
|
c154eb7e2b | ||
|
|
33686c6db8 | ||
|
|
6d6acbcfc1 | ||
|
|
4e2f2561ff |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
@@ -7,9 +7,7 @@ keywords: rich text, fields, config, configuration, documentation, Content Manag
|
||||
---
|
||||
|
||||
<Banner>
|
||||
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.
|
||||
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.
|
||||
</Banner>
|
||||
|
||||
<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.
|
||||
|
||||
<Banner type="success">
|
||||
<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.
|
||||
<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.
|
||||
</Banner>
|
||||
|
||||
### Config
|
||||
@@ -125,13 +116,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.
|
||||
|
||||
<Banner type="success">
|
||||
<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.
|
||||
<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.
|
||||
</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.
|
||||
@@ -307,10 +292,7 @@ const serialize = (children) =>
|
||||
```
|
||||
|
||||
<Banner>
|
||||
<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!
|
||||
<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!
|
||||
</Banner>
|
||||
|
||||
### Built-in SlateJS Plugins
|
||||
|
||||
@@ -16,6 +16,7 @@ Collections feature the ability to define the following hooks:
|
||||
- [afterRead](#afterread)
|
||||
- [beforeDelete](#beforedelete)
|
||||
- [afterDelete](#afterdelete)
|
||||
- [afterOperation](#afteroperation)
|
||||
|
||||
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.
|
||||
|
||||
`collections/exampleHooks.js`
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
@@ -48,6 +50,7 @@ export const ExampleHooks: CollectionConfig = {
|
||||
afterChange: [(args) => {...}],
|
||||
afterRead: [(args) => {...}],
|
||||
afterDelete: [(args) => {...}],
|
||||
afterOperation: [(args) => {...}],
|
||||
|
||||
// Auth-enabled hooks
|
||||
beforeLogin: [(args) => {...}],
|
||||
@@ -62,19 +65,19 @@ export const ExampleHooks: CollectionConfig = {
|
||||
|
||||
### 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
|
||||
import { CollectionBeforeOperationHook } from 'payload/types';
|
||||
import { CollectionBeforeOperationHook } from "payload/types";
|
||||
|
||||
const beforeOperationHook: CollectionBeforeOperationHook = async ({
|
||||
args, // Original arguments passed into the operation
|
||||
args, // original arguments passed into the operation
|
||||
operation, // name of the operation
|
||||
}) => {
|
||||
return args; // Return operation arguments as necessary
|
||||
}
|
||||
return args; // return modified operation arguments as necessary
|
||||
};
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeOperationHook } from 'payload/types';
|
||||
import { CollectionBeforeOperationHook } from "payload/types";
|
||||
|
||||
const beforeValidateHook: CollectionBeforeValidateHook = async ({
|
||||
data, // incoming data to update or create with
|
||||
@@ -97,7 +100,7 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({
|
||||
originalDoc, // original document
|
||||
}) => {
|
||||
return data; // Return data to either create or update a document with
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeChangeHook } from 'payload/types';
|
||||
import { CollectionBeforeChangeHook } from "payload/types";
|
||||
|
||||
const beforeChangeHook: CollectionBeforeChangeHook = async ({
|
||||
data, // incoming data to update or create with
|
||||
@@ -114,7 +117,7 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({
|
||||
originalDoc, // original document
|
||||
}) => {
|
||||
return data; // Return data to either create or update a document with
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```ts
|
||||
import { CollectionAfterChangeHook } from 'payload/types';
|
||||
import { CollectionAfterChangeHook } from "payload/types";
|
||||
|
||||
const afterChangeHook: CollectionAfterChangeHook = async ({
|
||||
doc, // full document data
|
||||
@@ -131,7 +134,7 @@ const afterChangeHook: CollectionAfterChangeHook = async ({
|
||||
operation, // name of the operation ie. 'create', 'update'
|
||||
}) => {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeReadHook } from 'payload/types';
|
||||
import { CollectionBeforeReadHook } from "payload/types";
|
||||
|
||||
const beforeReadHook: CollectionBeforeReadHook = async ({
|
||||
doc, // full document data
|
||||
@@ -147,7 +150,7 @@ const beforeReadHook: CollectionBeforeReadHook = async ({
|
||||
query, // JSON formatted query
|
||||
}) => {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```ts
|
||||
import { CollectionAfterReadHook } from 'payload/types';
|
||||
import { CollectionAfterReadHook } from "payload/types";
|
||||
|
||||
const afterReadHook: CollectionAfterReadHook = async ({
|
||||
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
|
||||
}) => {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
import { CollectionBeforeLoginHook } from 'payload/types';
|
||||
import { CollectionBeforeLoginHook } from "payload/types";
|
||||
|
||||
const beforeLoginHook: CollectionBeforeLoginHook = async ({
|
||||
req, // full express request
|
||||
user, // user being logged in
|
||||
}) => {
|
||||
return user;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### afterLogin
|
||||
@@ -267,7 +288,7 @@ const afterMeHook: CollectionAfterMeHook = async ({
|
||||
For auth-enabled Collections, this hook runs after successful `forgotPassword` operations. Returned values are discarded.
|
||||
|
||||
```ts
|
||||
import { CollectionAfterForgotPasswordHook } from 'payload/types';
|
||||
import { CollectionAfterForgotPasswordHook } from "payload/types";
|
||||
|
||||
const afterLoginHook: CollectionAfterForgotPasswordHook = async ({
|
||||
req, // full express request
|
||||
@@ -275,7 +296,7 @@ const afterLoginHook: CollectionAfterForgotPasswordHook = async ({
|
||||
token, // user token
|
||||
}) => {
|
||||
return user;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
@@ -298,5 +319,5 @@ import type {
|
||||
CollectionAfterRefreshHook,
|
||||
CollectionAfterMeHook,
|
||||
CollectionAfterForgotPasswordHook,
|
||||
} from 'payload/types';
|
||||
} from "payload/types";
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "1.13.4",
|
||||
"version": "1.14.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -46,6 +46,7 @@
|
||||
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||
"translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts",
|
||||
"clean:cache": "rimraf node_modules/.cache",
|
||||
"clean": "rimraf dist",
|
||||
"release:patch": "release-it patch",
|
||||
|
||||
128
scripts/translateNewKeys.ts
Normal file
128
scripts/translateNewKeys.ts
Normal 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;
|
||||
}
|
||||
@@ -72,6 +72,7 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>((props,
|
||||
iconPosition = 'right',
|
||||
newTab,
|
||||
tooltip,
|
||||
'aria-label': ariaLabel,
|
||||
} = props;
|
||||
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
@@ -101,6 +102,8 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>((props,
|
||||
type,
|
||||
className: classes,
|
||||
disabled,
|
||||
'aria-disabled': disabled,
|
||||
'aria-label': ariaLabel,
|
||||
onMouseEnter: tooltip ? () => setShowTooltip(true) : undefined,
|
||||
onMouseLeave: tooltip ? () => setShowTooltip(false) : undefined,
|
||||
onClick: !disabled ? handleClick : undefined,
|
||||
|
||||
@@ -19,4 +19,5 @@ export type Props = {
|
||||
iconPosition?: 'left' | 'right',
|
||||
newTab?: boolean
|
||||
tooltip?: string
|
||||
'aria-label'?: string
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
padding: base(1.25) $baseline;
|
||||
position: relative;
|
||||
|
||||
h5 {
|
||||
&__title {
|
||||
@extend %h5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -7,7 +7,7 @@ import './index.scss';
|
||||
const baseClass = 'card';
|
||||
|
||||
const Card: React.FC<Props> = (props) => {
|
||||
const { id, title, actions, onClick } = props;
|
||||
const { id, title, titleAs, buttonAriaLabel, actions, onClick } = props;
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -15,14 +15,16 @@ const Card: React.FC<Props> = (props) => {
|
||||
onClick && `${baseClass}--has-onclick`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const Tag = titleAs ?? 'div';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
id={id}
|
||||
>
|
||||
<h5>
|
||||
<Tag className={`${baseClass}__title`}>
|
||||
{title}
|
||||
</h5>
|
||||
</Tag>
|
||||
{actions && (
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{actions}
|
||||
@@ -30,6 +32,7 @@ const Card: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{onClick && (
|
||||
<Button
|
||||
aria-label={buttonAriaLabel}
|
||||
className={`${baseClass}__click`}
|
||||
buttonStyle="none"
|
||||
onClick={onClick}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ElementType } from 'react';
|
||||
|
||||
export type Props = {
|
||||
id?: string,
|
||||
title: string,
|
||||
titleAs?: ElementType,
|
||||
buttonAriaLabel?: string,
|
||||
actions?: React.ReactNode,
|
||||
onClick?: () => void,
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import type { Props } from './types';
|
||||
import { useTheme } from '../../utilities/Theme';
|
||||
import { ShimmerEffect } from '../ShimmerEffect';
|
||||
|
||||
import './index.scss';
|
||||
import { ShimmerEffect } from '../ShimmerEffect';
|
||||
|
||||
const baseClass = 'code-editor';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useId, useState } from 'react';
|
||||
import React, { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Pill from '../Pill';
|
||||
import Plus from '../../icons/Plus';
|
||||
@@ -61,6 +61,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
|
||||
alignIcon="left"
|
||||
key={`${collection.slug}-${col.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`}
|
||||
icon={active ? <X /> : <Plus />}
|
||||
aria-checked={active}
|
||||
className={[
|
||||
`${baseClass}__column`,
|
||||
active && `${baseClass}__column--active`,
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Props, TogglerProps } from './types';
|
||||
import { EditDepthContext, useEditDepth } from '../../utilities/EditDepth';
|
||||
import { Gutter } from '../Gutter';
|
||||
import './index.scss';
|
||||
import X from '../../icons/X';
|
||||
|
||||
const baseClass = 'drawer';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'drawer';
|
||||
const zBase = 100;
|
||||
|
||||
export const formatDrawerSlug = ({
|
||||
|
||||
@@ -38,6 +38,11 @@ const getUseAsTitle = (collection: SanitizedCollectionConfig) => {
|
||||
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 {
|
||||
collection,
|
||||
@@ -105,6 +110,8 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
pillStyle="light"
|
||||
className={`${baseClass}__toggle-columns ${visibleDrawer === 'columns' ? `${baseClass}__buttons-active` : ''}`}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)}
|
||||
aria-expanded={visibleDrawer === 'columns'}
|
||||
aria-controls={`${baseClass}-columns`}
|
||||
icon={<Chevron />}
|
||||
>
|
||||
{t('columns')}
|
||||
@@ -114,6 +121,8 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
pillStyle="light"
|
||||
className={`${baseClass}__toggle-where ${visibleDrawer === 'where' ? `${baseClass}__buttons-active` : ''}`}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)}
|
||||
aria-expanded={visibleDrawer === 'where'}
|
||||
aria-controls={`${baseClass}-where`}
|
||||
icon={<Chevron />}
|
||||
>
|
||||
{t('filters')}
|
||||
@@ -123,6 +132,8 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
className={`${baseClass}__toggle-sort`}
|
||||
buttonStyle={visibleDrawer === 'sort' ? undefined : 'secondary'}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
|
||||
aria-expanded={visibleDrawer === 'sort'}
|
||||
aria-controls={`${baseClass}-sort`}
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
@@ -136,6 +147,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__columns`}
|
||||
height={visibleDrawer === 'columns' ? 'auto' : 0}
|
||||
id={`${baseClass}-columns`}
|
||||
>
|
||||
<ColumnSelector collection={collection} />
|
||||
</AnimateHeight>
|
||||
@@ -143,6 +155,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__where`}
|
||||
height={visibleDrawer === 'where' ? 'auto' : 0}
|
||||
id={`${baseClass}-where`}
|
||||
>
|
||||
<WhereBuilder
|
||||
collection={collection}
|
||||
@@ -154,6 +167,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__sort`}
|
||||
height={visibleDrawer === 'sort' ? 'auto' : 0}
|
||||
id={`${baseClass}-sort`}
|
||||
>
|
||||
<SortComplex
|
||||
modifySearchQuery={modifySearchQuery}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import LogOut from '../../icons/LogOut';
|
||||
@@ -7,16 +8,21 @@ import LogOut from '../../icons/LogOut';
|
||||
const baseClass = 'nav';
|
||||
|
||||
const DefaultLogout = () => {
|
||||
const { t } = useTranslation('authentication');
|
||||
const config = useConfig();
|
||||
const {
|
||||
routes: { admin },
|
||||
admin: {
|
||||
logoutRoute,
|
||||
components: { logout }
|
||||
}
|
||||
components: { logout },
|
||||
},
|
||||
} = config;
|
||||
return (
|
||||
<Link to={`${admin}${logoutRoute}`} className={`${baseClass}__log-out`}>
|
||||
<Link
|
||||
to={`${admin}${logoutRoute}`}
|
||||
className={`${baseClass}__log-out`}
|
||||
aria-label={t('logOut')}
|
||||
>
|
||||
<LogOut />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ const DefaultNav = () => {
|
||||
const [menuActive, setMenuActive] = useState(false);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const history = useHistory();
|
||||
const { i18n } = useTranslation('general');
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
@@ -81,6 +81,7 @@ const DefaultNav = () => {
|
||||
<Link
|
||||
to={admin}
|
||||
className={`${baseClass}__brand`}
|
||||
aria-label={t('dashboard')}
|
||||
>
|
||||
<Icon />
|
||||
</Link>
|
||||
@@ -141,6 +142,7 @@ const DefaultNav = () => {
|
||||
<Link
|
||||
to={`${admin}/account`}
|
||||
className={`${baseClass}__account`}
|
||||
aria-label={t('authentication:account')}
|
||||
>
|
||||
<Account />
|
||||
</Link>
|
||||
|
||||
@@ -45,6 +45,10 @@ const StaticPill: React.FC<Props> = (props) => {
|
||||
children,
|
||||
elementProps,
|
||||
rounded,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-expanded': ariaExpanded,
|
||||
'aria-controls': ariaControls,
|
||||
'aria-checked': ariaChecked,
|
||||
} = props;
|
||||
|
||||
const classes = [
|
||||
@@ -67,6 +71,10 @@ const StaticPill: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<Element
|
||||
{...elementProps}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={ariaExpanded}
|
||||
aria-controls={ariaControls}
|
||||
aria-checked={ariaChecked}
|
||||
className={classes}
|
||||
type={Element === 'button' ? 'button' : undefined}
|
||||
to={to || undefined}
|
||||
|
||||
@@ -11,6 +11,10 @@ export type Props = {
|
||||
draggable?: boolean,
|
||||
rounded?: boolean
|
||||
id?: string
|
||||
'aria-label'?: string,
|
||||
'aria-expanded'?: boolean,
|
||||
'aria-controls'?: string,
|
||||
'aria-checked'?: boolean,
|
||||
elementProps?: HTMLAttributes<HTMLElement> & {
|
||||
ref: React.RefCallback<HTMLElement>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { components as SelectComponents, MultiValueProps } from 'react-select';
|
||||
import type { Option } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-label';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MultiValueRemoveProps } from 'react-select';
|
||||
import X from '../../../icons/X';
|
||||
import Tooltip from '../../Tooltip';
|
||||
import { Option as OptionType } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-remove';
|
||||
|
||||
@@ -43,6 +43,7 @@ export type OptionGroup = {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
inputId?: string
|
||||
className?: string
|
||||
value?: Option | Option[],
|
||||
onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -18,7 +18,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
const params = useSearchParams();
|
||||
const history = useHistory();
|
||||
const { i18n } = useTranslation();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const { sort } = params;
|
||||
|
||||
@@ -50,6 +50,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
buttonStyle="none"
|
||||
className={ascClasses.join(' ')}
|
||||
onClick={() => setSort(asc)}
|
||||
aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('ascending') })}
|
||||
>
|
||||
<Chevron />
|
||||
</Button>
|
||||
@@ -58,6 +59,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
buttonStyle="none"
|
||||
className={descClasses.join(' ')}
|
||||
onClick={() => setSort(desc)}
|
||||
aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('descending') })}
|
||||
>
|
||||
<Chevron />
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props, isComponent } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const ViewDescription: React.FC<Props> = (props) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import validateWhereQuery from './validateWhereQuery';
|
||||
import { Where } from '../../../../types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import { transformWhereQuery } from './transformWhereQuery';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -43,6 +44,10 @@ const reduceFields = (fields, i18n) => flattenTopLevelFields(fields).reduce((red
|
||||
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 {
|
||||
collection,
|
||||
@@ -59,16 +64,30 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
const params = useSearchParams();
|
||||
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) => {
|
||||
if (modifySearchQuery && validateWhereQuery(whereFromSearch)) {
|
||||
return whereFromSearch.or;
|
||||
}
|
||||
if (modifySearchQuery && whereFromSearch) {
|
||||
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 [];
|
||||
});
|
||||
|
||||
const [reducedFields] = useState(() => reduceFields(collection.fields, i18n));
|
||||
|
||||
// This handles updating the search query (URL) when the where conditions change
|
||||
useThrottledEffect(() => {
|
||||
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 = {
|
||||
...typeof currentParams?.where === 'object' ? currentParams.where : {},
|
||||
...typeof currentParams?.where === 'object' && (validateWhereQuery(currentParams?.where) || !hasNewWhereConditions) ? currentParams.where : {},
|
||||
or: [
|
||||
...conditions,
|
||||
...paramsToKeep,
|
||||
@@ -94,7 +116,6 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
if (handleChange) handleChange(newWhereQuery as Where);
|
||||
|
||||
const hasExistingConditions = typeof currentParams?.where === 'object' && 'or' in currentParams.where;
|
||||
const hasNewWhereConditions = conditions.length > 0;
|
||||
|
||||
if (modifySearchQuery && ((hasExistingConditions && !hasNewWhereConditions) || hasNewWhereConditions)) {
|
||||
history.replace({
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 => {
|
||||
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;
|
||||
|
||||
@@ -1,62 +1,74 @@
|
||||
import React from 'react';
|
||||
import Check from '../../../icons/Check';
|
||||
import Label from '../../Label';
|
||||
import Line from '../../../icons/Line';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'custom-checkbox';
|
||||
|
||||
type CheckboxInputProps = {
|
||||
onToggle: React.MouseEventHandler<HTMLButtonElement>
|
||||
onToggle: React.FormEventHandler<HTMLInputElement>
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement>
|
||||
readOnly?: boolean
|
||||
checked?: boolean
|
||||
partialChecked?: boolean
|
||||
name?: string
|
||||
id?: string
|
||||
label?: string
|
||||
'aria-label'?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
const {
|
||||
onToggle,
|
||||
checked,
|
||||
partialChecked,
|
||||
inputRef,
|
||||
name,
|
||||
id,
|
||||
label,
|
||||
'aria-label': ariaLabel,
|
||||
readOnly,
|
||||
required,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
checked && `${baseClass}--checked`,
|
||||
(checked || partialChecked) && `${baseClass}--checked`,
|
||||
readOnly && `${baseClass}--read-only`,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={checked}
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span className={`${baseClass}__input`}>
|
||||
<Check />
|
||||
<div className={`${baseClass}__input`}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
aria-label={ariaLabel}
|
||||
checked={Boolean(checked)}
|
||||
readOnly
|
||||
onInput={onToggle}
|
||||
/>
|
||||
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
|
||||
{!partialChecked && (
|
||||
<Check />
|
||||
)}
|
||||
{partialChecked && (
|
||||
<Line />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tooltip:not([aria-hidden="true"]) {
|
||||
right: auto;
|
||||
position: relative;
|
||||
@@ -22,33 +18,62 @@
|
||||
|
||||
|
||||
.custom-checkbox {
|
||||
display: inline-flex;
|
||||
|
||||
label {
|
||||
padding-bottom: 0;
|
||||
padding-left: base(.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
input {
|
||||
// hidden HTML checkbox
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
svg {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
// visible checkbox
|
||||
@include formInput;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
width: $baseline;
|
||||
height: $baseline;
|
||||
margin-right: base(.5);
|
||||
|
||||
& input[type="checkbox"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
border-radius: 0;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 3px 3px var(--theme-success-400);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--checked {
|
||||
.custom-checkbox__icon {
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--read-only {
|
||||
.custom-checkbox__input {
|
||||
background-color: var(--theme-elevation-100);
|
||||
@@ -58,40 +83,6 @@
|
||||
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] {
|
||||
|
||||
@@ -10,9 +10,9 @@ import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { Option } from '../../../elements/ReactSelect/types';
|
||||
import ReactSelect from '../../../elements/ReactSelect';
|
||||
import { isNumber } from '../../../../../utilities/isNumber';
|
||||
|
||||
import './index.scss';
|
||||
import { isNumber } from '../../../../../utilities/isNumber';
|
||||
|
||||
const NumberField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
@@ -145,7 +145,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
isClearable
|
||||
filterOption={(option, rawInput) => {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
return isNumber(rawInput)
|
||||
return isNumber(rawInput);
|
||||
}}
|
||||
numberOnly
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Tooltip from '../../../../../elements/Tooltip';
|
||||
import Edit from '../../../../../icons/Edit';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import { Option } from '../../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'relationship--multi-value-label';
|
||||
|
||||
@@ -6,11 +6,11 @@ import FormSubmit from '../../../../../Submit';
|
||||
import { Props } from './types';
|
||||
import fieldTypes from '../../../..';
|
||||
import RenderFields from '../../../../../RenderFields';
|
||||
|
||||
import './index.scss';
|
||||
import useHotkey from '../../../../../../../hooks/useHotkey';
|
||||
import { useEditDepth } from '../../../../../../utilities/EditDepth';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'rich-text-link-edit-modal';
|
||||
|
||||
export const LinkDrawer: React.FC<Props> = ({
|
||||
|
||||
@@ -130,9 +130,11 @@ const DefaultAccount: React.FC<Props> = (props) => {
|
||||
<h3>{t('general:payloadSettings')}</h3>
|
||||
<div className={`${baseClass}__language`}>
|
||||
<Label
|
||||
htmlFor="language-select"
|
||||
label={t('general:language')}
|
||||
/>
|
||||
<ReactSelect
|
||||
inputId="language-select"
|
||||
value={languageOptions.find((language) => (language.value === i18n.language))}
|
||||
options={languageOptions}
|
||||
onChange={({ value }) => (i18n.changeLanguage(value))}
|
||||
|
||||
@@ -24,7 +24,7 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
|
||||
const { push } = useHistory();
|
||||
const { i18n } = useTranslation('general');
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const {
|
||||
routes: {
|
||||
@@ -77,12 +77,14 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{entities.map(({ entity, type }, entityIndex) => {
|
||||
let title: string;
|
||||
let buttonAriaLabel: string;
|
||||
let createHREF: string;
|
||||
let onClick: () => void;
|
||||
let hasCreatePermission: boolean;
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
title = getTranslation(entity.labels.plural, i18n);
|
||||
buttonAriaLabel = t('showAllLabel', { label: title });
|
||||
onClick = () => push({ pathname: `${admin}/collections/${entity.slug}` });
|
||||
createHREF = `${admin}/collections/${entity.slug}/create`;
|
||||
hasCreatePermission = permissions?.collections?.[entity.slug]?.create?.permission;
|
||||
@@ -90,6 +92,7 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
|
||||
if (type === EntityType.global) {
|
||||
title = getTranslation(entity.label, i18n);
|
||||
buttonAriaLabel = t('editLabel', { label: getTranslation(entity.label, i18n) });
|
||||
onClick = () => push({ pathname: `${admin}/globals/${entity.slug}` });
|
||||
}
|
||||
|
||||
@@ -97,8 +100,10 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
<li key={entityIndex}>
|
||||
<Card
|
||||
title={title}
|
||||
titleAs="h3"
|
||||
id={`card-${entity.slug}`}
|
||||
onClick={onClick}
|
||||
buttonAriaLabel={buttonAriaLabel}
|
||||
actions={(hasCreatePermission && type === EntityType.collection) ? (
|
||||
<Button
|
||||
el="link"
|
||||
@@ -107,6 +112,7 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
aria-label={t('createNewLabel', { label: getTranslation(entity.labels.singular, i18n) })}
|
||||
/>
|
||||
) : undefined}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,6 @@ import FormSubmit from '../../forms/Submit';
|
||||
import Button from '../../elements/Button';
|
||||
import Meta from '../../utilities/Meta';
|
||||
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'forgot-password';
|
||||
|
||||
@@ -13,7 +13,6 @@ import Button from '../../elements/Button';
|
||||
import Meta from '../../utilities/Meta';
|
||||
import HiddenInput from '../../forms/field-types/HiddenInput';
|
||||
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'reset-password';
|
||||
|
||||
@@ -100,7 +100,10 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
{getTranslation(pluralLabel, i18n)}
|
||||
</h1>
|
||||
{hasCreatePermission && (
|
||||
<Pill to={newDocumentURL}>
|
||||
<Pill
|
||||
to={newDocumentURL}
|
||||
aria-label={t('createNewLabel', { label: getTranslation(singularLabel, i18n) })}
|
||||
>
|
||||
{t('createNew')}
|
||||
</Pill>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,22 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SelectAllStatus, useSelection } from '../SelectionProvider';
|
||||
import Check from '../../../../icons/Check';
|
||||
import Line from '../../../../icons/Line';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'select-all';
|
||||
import { CheckboxInput } from '../../../../forms/field-types/Checkbox/Input';
|
||||
|
||||
const SelectAll: React.FC = () => {
|
||||
const { t } = useTranslation('general');
|
||||
const { selectAll, toggleAll } = useSelection();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAll()}
|
||||
>
|
||||
<span className={`${baseClass}__input`}>
|
||||
{ (selectAll === SelectAllStatus.AllInPage || selectAll === SelectAllStatus.AllAvailable) && (
|
||||
<Check />
|
||||
)}
|
||||
{ selectAll === SelectAllStatus.Some && (
|
||||
<Line />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<CheckboxInput
|
||||
id="select-all"
|
||||
aria-label={selectAll === SelectAllStatus.None ? t('selectAllRows') : t('deselectAllRows')}
|
||||
checked={selectAll === SelectAllStatus.AllInPage || selectAll === SelectAllStatus.AllAvailable}
|
||||
partialChecked={selectAll === SelectAllStatus.Some}
|
||||
onToggle={() => toggleAll()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useSelection } from '../SelectionProvider';
|
||||
import Check from '../../../../icons/Check';
|
||||
import { CheckboxInput } from '../../../../forms/field-types/Checkbox/Input';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'select-row';
|
||||
|
||||
const SelectRow: React.FC<{ id: string | number }> = ({ id }) => {
|
||||
const { selected, setSelection } = useSelection();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
(selected[id]) && `${baseClass}--checked`,
|
||||
].filter(Boolean).join(' ')}
|
||||
key={id}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelection(id)}
|
||||
>
|
||||
<span className={`${baseClass}__input`}>
|
||||
<Check />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<CheckboxInput
|
||||
checked={selected[id]}
|
||||
onToggle={() => setSelection(id)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ import { useSearchParams } from '../../../utilities/SearchParams';
|
||||
import { TableColumnsProvider } from '../../../elements/TableColumns';
|
||||
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 {
|
||||
collection,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Document } from 'mongoose';
|
||||
import { APIError } from '../../errors';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Collection } from '../../collections/config/types';
|
||||
import { buildAfterOperation } from '../../collections/operations/utils';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -128,6 +129,16 @@ async function forgotPassword(incomingArgs: Arguments): Promise<string | null> {
|
||||
await hook({ args, context: req.context });
|
||||
}, Promise.resolve());
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
token = await buildAfterOperation({
|
||||
operation: 'forgotPassword',
|
||||
args,
|
||||
result: token,
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { User } from '../types';
|
||||
import { Collection } from '../../collections/config/types';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import unlock from './unlock';
|
||||
import { buildAfterOperation } from '../../collections/operations/utils';
|
||||
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts';
|
||||
import { authenticateLocalStrategy } from '../strategies/local/authenticate';
|
||||
import { getFieldsToSign } from './getFieldsToSign';
|
||||
@@ -206,15 +207,28 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
}) || user;
|
||||
}, Promise.resolve());
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
return {
|
||||
let result: Result & { user: GeneratedTypes['collections'][TSlug] } = {
|
||||
token,
|
||||
user,
|
||||
exp: (jwt.decode(token) as jwt.JwtPayload).exp,
|
||||
};
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
|
||||
operation: 'login',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default login;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import url from 'url';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Response } from 'express';
|
||||
import url from 'url';
|
||||
import { Collection, BeforeOperationHook } from '../../collections/config/types';
|
||||
import { Forbidden } from '../../errors';
|
||||
import getCookieExpiration from '../../utilities/getCookieExpiration';
|
||||
import { Document } from '../../types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { buildAfterOperation } from '../../collections/operations/utils';
|
||||
import { getFieldsToSign } from './getFieldsToSign';
|
||||
|
||||
export type Result = {
|
||||
@@ -97,7 +98,7 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
|
||||
args.res.cookie(`${config.cookiePrefix}-token`, refreshedToken, cookieOptions);
|
||||
}
|
||||
|
||||
let response: Result = {
|
||||
let result: Result = {
|
||||
user,
|
||||
refreshedToken,
|
||||
exp,
|
||||
@@ -110,20 +111,31 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
|
||||
await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => {
|
||||
await priorHook;
|
||||
|
||||
response = (await hook({
|
||||
result = (await hook({
|
||||
req: args.req,
|
||||
res: args.res,
|
||||
exp,
|
||||
token: refreshedToken,
|
||||
context: args.req.context,
|
||||
})) || response;
|
||||
})) || result;
|
||||
}, Promise.resolve());
|
||||
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation({
|
||||
operation: 'refresh',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
return response;
|
||||
return result;
|
||||
}
|
||||
|
||||
export default refresh;
|
||||
|
||||
@@ -16,7 +16,7 @@ const swcOptions = {
|
||||
tsx: true,
|
||||
},
|
||||
paths: undefined,
|
||||
baseUrl: __dirname,
|
||||
baseUrl: path.resolve(),
|
||||
},
|
||||
module: {
|
||||
type: 'commonjs',
|
||||
|
||||
@@ -29,6 +29,7 @@ export const defaults = {
|
||||
afterRead: [],
|
||||
beforeDelete: [],
|
||||
afterDelete: [],
|
||||
afterOperation: [],
|
||||
beforeLogin: [],
|
||||
afterLogin: [],
|
||||
afterLogout: [],
|
||||
|
||||
@@ -98,6 +98,7 @@ const collectionSchema = joi.object().keys({
|
||||
afterRead: joi.array().items(joi.func()),
|
||||
beforeDelete: joi.array().items(joi.func()),
|
||||
afterDelete: joi.array().items(joi.func()),
|
||||
afterOperation: joi.array().items(joi.func()),
|
||||
beforeLogin: joi.array().items(joi.func()),
|
||||
afterLogin: joi.array().items(joi.func()),
|
||||
afterLogout: joi.array().items(joi.func()),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { IncomingUploadType, Upload } from '../../uploads/types';
|
||||
import { IncomingCollectionVersions, SanitizedCollectionVersions } from '../../versions/types';
|
||||
import { BuildQueryArgs } from '../../mongoose/buildQuery';
|
||||
import { CustomPreviewButtonProps, CustomPublishButtonProps, CustomSaveButtonProps, CustomSaveDraftButtonProps } 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 EditProps } from '../../admin/components/views/collections/Edit/types';
|
||||
|
||||
@@ -123,6 +124,13 @@ export type AfterDeleteHook<T extends TypeWithID = any> = (args: {
|
||||
context: RequestContext;
|
||||
}) => 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 BeforeLoginHook<T extends TypeWithID = any> = (args: {
|
||||
@@ -314,6 +322,7 @@ export type CollectionConfig = {
|
||||
afterMe?: AfterMeHook[];
|
||||
afterRefresh?: AfterRefreshHook[];
|
||||
afterForgotPassword?: AfterForgotPasswordHook[];
|
||||
afterOperation?: AfterOperationHook[];
|
||||
};
|
||||
/**
|
||||
* Custom rest api endpoints
|
||||
|
||||
@@ -22,11 +22,14 @@ import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { generateFileData } from '../../uploads/generateFileData';
|
||||
import { saveVersion } from '../../versions/saveVersion';
|
||||
import { mapAsync } from '../../utilities/mapAsync';
|
||||
import { buildAfterOperation } from './utils';
|
||||
import { registerLocalStrategy } from '../../auth/strategies/local/register';
|
||||
|
||||
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
|
||||
req: PayloadRequest
|
||||
depth?: number
|
||||
@@ -318,6 +321,16 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
}) || result;
|
||||
}, 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
|
||||
if (config.upload?.useTempFiles && collectionConfig.upload) {
|
||||
const { files } = req;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Where } from '../../types';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions';
|
||||
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles';
|
||||
import { buildAfterOperation } from './utils';
|
||||
|
||||
export type Arguments = {
|
||||
depth?: number
|
||||
@@ -212,10 +213,22 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
|
||||
}
|
||||
preferences.Model.deleteMany({ key: { in: docs.map(({ id }) => `collection-${collectionConfig.slug}-${id}`) } });
|
||||
|
||||
return {
|
||||
let result = {
|
||||
docs: awaitedDocs.filter(Boolean),
|
||||
errors,
|
||||
};
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
|
||||
operation: 'delete',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default deleteOperation;
|
||||
|
||||
@@ -4,11 +4,12 @@ import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
|
||||
import { NotFound, Forbidden } from '../../errors';
|
||||
import executeAccess from '../../auth/executeAccess';
|
||||
import { BeforeOperationHook, Collection } from '../config/types';
|
||||
import { Document, Where } from '../../types';
|
||||
import { Document } from '../../types';
|
||||
import { hasWhereAccessResult } from '../../auth/types';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions';
|
||||
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles';
|
||||
import { buildAfterOperation } from './utils';
|
||||
|
||||
export type Arguments = {
|
||||
depth?: number
|
||||
@@ -174,6 +175,16 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(inc
|
||||
result = await hook({ req, id, doc: result, context: req.context }) || result;
|
||||
}, Promise.resolve());
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
|
||||
operation: 'deleteByID',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// 8. Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -9,6 +9,7 @@ import { buildSortParam } from '../../mongoose/buildSortParam';
|
||||
import { AccessResult } from '../../config/types';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { queryDrafts } from '../../versions/drafts/queryDrafts';
|
||||
import { buildAfterOperation } from './utils';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -226,6 +227,16 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
})),
|
||||
};
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation<T>({
|
||||
operation: 'find',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NotFound } from '../../errors';
|
||||
import executeAccess from '../../auth/executeAccess';
|
||||
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable';
|
||||
import { afterRead } from '../../fields/hooks/afterRead';
|
||||
import { buildAfterOperation } from './utils';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -173,6 +174,16 @@ async function findByID<T extends TypeWithID>(
|
||||
}) || result;
|
||||
}, Promise.resolve());
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation<T>({
|
||||
operation: 'findByID',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -18,8 +18,10 @@ import { AccessResult } from '../../config/types';
|
||||
import { queryDrafts } from '../../versions/drafts/queryDrafts';
|
||||
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles';
|
||||
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles';
|
||||
import { buildAfterOperation } from './utils';
|
||||
import { CreateUpdateType } from './create';
|
||||
|
||||
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = {
|
||||
export type Arguments<T extends CreateUpdateType> = {
|
||||
collection: Collection
|
||||
req: PayloadRequest
|
||||
where: Where
|
||||
@@ -350,10 +352,22 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
|
||||
const awaitedDocs = await Promise.all(promises);
|
||||
|
||||
return {
|
||||
let result = {
|
||||
docs: awaitedDocs.filter(Boolean),
|
||||
errors,
|
||||
};
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
|
||||
operation: 'update',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default update;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { generateFileData } from '../../uploads/generateFileData';
|
||||
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion';
|
||||
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles';
|
||||
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles';
|
||||
import { buildAfterOperation } from './utils';
|
||||
import { generatePasswordSaltHash } from '../../auth/strategies/local/generatePasswordSaltHash';
|
||||
|
||||
export type Arguments<T extends { [field: string | number | symbol]: unknown }> = {
|
||||
@@ -340,6 +341,16 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
}) || result;
|
||||
}, Promise.resolve());
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
|
||||
operation: 'updateByID',
|
||||
args,
|
||||
result,
|
||||
});
|
||||
|
||||
await unlinkTempFiles({
|
||||
req,
|
||||
config,
|
||||
|
||||
71
src/collections/operations/utils.ts
Normal file
71
src/collections/operations/utils.ts
Normal 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;
|
||||
};
|
||||
@@ -3,10 +3,8 @@ import merge from 'deepmerge';
|
||||
import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types';
|
||||
import { Operation } from '../../../types';
|
||||
import { PayloadRequest, RequestContext } from '../../../express/types';
|
||||
import getValueWithDefault from '../../getDefaultValue';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import { getExistingRowDoc } from './getExistingRowDoc';
|
||||
import { cloneDataFromOriginalDoc } from './cloneDataFromOriginalDoc';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
@@ -28,8 +26,6 @@ type Args = {
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Run condition
|
||||
// - Merge original document data into incoming data
|
||||
// - Compute default values for undefined fields
|
||||
// - Execute field hooks
|
||||
// - Validate data
|
||||
// - Transform data for storage
|
||||
@@ -59,26 +55,6 @@ export const promise = async ({
|
||||
const operationLocale = req.locale || defaultLocale;
|
||||
|
||||
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
|
||||
if (field.localized && operationLocale !== defaultLocale) {
|
||||
if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { PayloadRequest, RequestContext } from '../../../express/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';
|
||||
|
||||
type Args<T> = {
|
||||
@@ -20,6 +23,8 @@ type Args<T> = {
|
||||
// - Sanitize incoming data
|
||||
// - Execute field hooks
|
||||
// - Execute field access control
|
||||
// - Merge original document data into incoming data
|
||||
// - Compute default values for undefined fields
|
||||
|
||||
export const promise = async <T>({
|
||||
data,
|
||||
@@ -189,6 +194,22 @@ export const promise = async <T>({
|
||||
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
|
||||
@@ -231,7 +252,7 @@ export const promise = async <T>({
|
||||
overrideAccess,
|
||||
req,
|
||||
siblingData: row,
|
||||
siblingDoc: siblingDoc[field.name]?.[i] || {},
|
||||
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
||||
context,
|
||||
}));
|
||||
});
|
||||
@@ -258,7 +279,7 @@ export const promise = async <T>({
|
||||
overrideAccess,
|
||||
req,
|
||||
siblingData: row,
|
||||
siblingDoc: siblingDoc[field.name]?.[i] || {},
|
||||
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
|
||||
context,
|
||||
}));
|
||||
}
|
||||
@@ -291,8 +312,11 @@ export const promise = async <T>({
|
||||
let tabSiblingData;
|
||||
let tabSiblingDoc;
|
||||
if (tabHasName(field)) {
|
||||
tabSiblingData = typeof siblingData[field.name] === 'object' ? siblingData[field.name] : {};
|
||||
tabSiblingDoc = typeof siblingDoc[field.name] === 'object' ? siblingDoc[field.name] : {};
|
||||
if (typeof siblingData[field.name] !== 'object') siblingData[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 {
|
||||
tabSiblingData = siblingData;
|
||||
tabSiblingDoc = siblingDoc;
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "حدث خطأ أثناء حذف الملف.",
|
||||
"deletingTitle": "حدث خطأ أثناء حذف {{title}}. يرجى التحقق من الاتصال الخاص بك والمحاولة مرة أخرى.",
|
||||
"emailOrPasswordIncorrect": "البريد الإلكتروني أو كلمة المرور المقدمة غير صحيحة.",
|
||||
"followingFieldsInvalid_other": "الحقول التالية غير صالحة:",
|
||||
"followingFieldsInvalid_one": "الحقل التالي غير صالح:",
|
||||
"followingFieldsInvalid_other": "الحقول التالية غير صالحة:",
|
||||
"incorrectCollection": "مجموعة غير صحيحة",
|
||||
"invalidFileType": "نوع ملف غير صالح",
|
||||
"invalidFileTypeValue": "نوع ملف غير صالح: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "تم الحذف بنجاح.",
|
||||
"deleting": "جاري الحذف...",
|
||||
"descending": "تنازلي",
|
||||
"deselectAllRows": "إلغاء تحديد جميع الصفوف",
|
||||
"duplicate": "استنساخ",
|
||||
"duplicateWithoutSaving": "استنساخ بدون حفظ التغييرات",
|
||||
"edit": "تعديل",
|
||||
@@ -181,11 +182,11 @@
|
||||
"editingLabel_many": "تعديل {{count}} {{label}}",
|
||||
"editingLabel_one": "تعديل {{count}} {{label}}",
|
||||
"editingLabel_other": "تعديل {{count}} {{label}}",
|
||||
"error": "خطأ",
|
||||
"errors": "أخطاء",
|
||||
"email": "البريد الإلكتروني",
|
||||
"emailAddress": "عنوان البريد الإلكتروني",
|
||||
"enterAValue": "أدخل قيمة",
|
||||
"error": "خطأ",
|
||||
"errors": "أخطاء",
|
||||
"fallbackToDefaultLocale": "الرجوع إلى اللغة الافتراضية",
|
||||
"filter": "تصفية",
|
||||
"filterWhere": "تصفية {{label}} حيث",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "جاري الحفظ...",
|
||||
"searchBy": "البحث عن طريق {{label}}",
|
||||
"selectAll": "تحديد كل {{count}} {{label}}",
|
||||
"selectAllRows": "حدد جميع الصفوف",
|
||||
"selectValue": "اختيار قيمة",
|
||||
"selectedCount": "تم تحديد {{count}} {{label}}",
|
||||
"showAllLabel": "عرض كل {{label}}",
|
||||
"sorryNotFound": "عذرًا - لا يوجد شيء يتوافق مع طلبك.",
|
||||
"sort": "ترتيب",
|
||||
"sortByLabelDirection": "رتّب حسب {{label}} {{direction}}",
|
||||
"stayOnThisPage": "البقاء على هذه الصفحة",
|
||||
"submissionSuccessful": "تمت الإرسال بنجاح.",
|
||||
"submit": "إرسال",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "عرض النسخ لـ {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "عرض النسخ للـ {{entityLabel}} العام"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@
|
||||
"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_one": "Aşağıdakı sahə yanlışdır:",
|
||||
"followingFieldsInvalid_other": "Aşağıdaki sahələr yanlışdır:",
|
||||
"incorrectCollection": "Yanlış Kolleksiya",
|
||||
"invalidFileType": "Yanlış fayl növü",
|
||||
"invalidFileTypeValue": "Yanlış fayl növü: {{value}}",
|
||||
@@ -173,6 +174,7 @@
|
||||
"deletedSuccessfully": "Uğurla silindi.",
|
||||
"deleting": "Silinir...",
|
||||
"descending": "Azalan",
|
||||
"deselectAllRows": "Bütün sıraları seçimi ləğv edin",
|
||||
"duplicate": "Dublikat",
|
||||
"duplicateWithoutSaving": "Dəyişiklikləri saxlamadan dublikatla",
|
||||
"edit": "Redaktə et",
|
||||
@@ -222,10 +224,13 @@
|
||||
"saving": "Saxlanılır...",
|
||||
"searchBy": "{{label}} ilə axtar",
|
||||
"selectAll": "Bütün {{count}} {{label}} seç",
|
||||
"selectAllRows": "Bütün sıraları seçin",
|
||||
"selectValue": "Dəyər seçin",
|
||||
"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.",
|
||||
"sort": "Sırala",
|
||||
"sortByLabelDirection": "{{label}} {{direction}} ilə sırala",
|
||||
"stayOnThisPage": "Bu səhifədə qal",
|
||||
"submissionSuccessful": "Təqdimat uğurlu oldu.",
|
||||
"submit": "Təqdim et",
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Имаше грешка при изтриването на файла.",
|
||||
"deletingTitle": "Имаше проблем при изтриването на {{title}}. Моля провери връзката си и опитай отново.",
|
||||
"emailOrPasswordIncorrect": "Имейлът или паролата не са правилни.",
|
||||
"followingFieldsInvalid_other": "Следните полета са некоректни:",
|
||||
"followingFieldsInvalid_one": "Следното поле е некоректно:",
|
||||
"followingFieldsInvalid_other": "Следните полета са некоректни:",
|
||||
"incorrectCollection": "Некоректно събиране",
|
||||
"invalidFileType": "Невалиден тип на файл",
|
||||
"invalidFileTypeValue": "Невалиден тип на файл: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Изтрито успешно.",
|
||||
"deleting": "Изтриване...",
|
||||
"descending": "Низходящо",
|
||||
"deselectAllRows": "Деселектирайте всички редове",
|
||||
"duplicate": "Дупликирай",
|
||||
"duplicateWithoutSaving": "Дупликирай без да запазваш промените",
|
||||
"edit": "Редактирай",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Запазване...",
|
||||
"searchBy": "Търси по {{label}}",
|
||||
"selectAll": "Избери всички {{count}} {{label}}",
|
||||
"selectAllRows": "Изберете всички редове",
|
||||
"selectValue": "Избери стойност",
|
||||
"selectedCount": "{{count}} {{label}} избрани",
|
||||
"showAllLabel": "Покажи всички {{label}}",
|
||||
"sorryNotFound": "Съжаляваме-няма нищо, което да отговаря на търсенето ти.",
|
||||
"sort": "Сортирай",
|
||||
"sortByLabelDirection": "Сортирай по {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Остани на тази страница",
|
||||
"submissionSuccessful": "Успешно подаване.",
|
||||
"submit": "Подай",
|
||||
@@ -261,6 +265,7 @@
|
||||
"near": "близко"
|
||||
},
|
||||
"upload": {
|
||||
"dragAndDrop": "Дръпни и пусни файл",
|
||||
"dragAndDropHere": "или дръпни и пусни файла тук",
|
||||
"fileName": "Име на файла",
|
||||
"fileSize": "Големина на файла",
|
||||
@@ -269,7 +274,6 @@
|
||||
"moreInfo": "Повече информация",
|
||||
"selectCollectionToBrowse": "Избери колекция, която да разгледаш",
|
||||
"selectFile": "Избери файл",
|
||||
"dragAndDrop": "Дръпни и пусни файл",
|
||||
"sizes": "Големини",
|
||||
"width": "Ширина"
|
||||
},
|
||||
@@ -327,8 +331,8 @@
|
||||
"saveDraft": "Запази чернова",
|
||||
"selectLocales": "Избери локализации за показване",
|
||||
"selectVersionToCompare": "Избери версия за сравняване",
|
||||
"showingVersionsFor": "Показване на версии за:",
|
||||
"showLocales": "Покажи преводи:",
|
||||
"showingVersionsFor": "Показване на версии за:",
|
||||
"status": "Статус",
|
||||
"type": "Тип",
|
||||
"unpublish": "Скрий",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Гледане на версии за {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Гледане на версии за глобалния документ {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"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.",
|
||||
"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_other": "Následující pole jsou neplatná:",
|
||||
"incorrectCollection": "Nesprávná kolekce",
|
||||
"invalidFileType": "Neplatný typ souboru",
|
||||
"invalidFileTypeValue": "Neplatný typ souboru: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Úspěšně odstraněno.",
|
||||
"deleting": "Odstraňování...",
|
||||
"descending": "Sestupně",
|
||||
"deselectAllRows": "Zrušte výběr všech řádků",
|
||||
"duplicate": "Duplikovat",
|
||||
"duplicateWithoutSaving": "Duplikovat bez uložení změn",
|
||||
"edit": "Upravit",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Ukládání...",
|
||||
"searchBy": "Vyhledat podle {{label}}",
|
||||
"selectAll": "Vybrat vše {{count}} {{label}}",
|
||||
"selectAllRows": "Vyberte všechny řádky",
|
||||
"selectValue": "Vyberte hodnotu",
|
||||
"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.",
|
||||
"sort": "Třídit",
|
||||
"sortByLabelDirection": "Seřadit podle {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Zůstat na této stránce",
|
||||
"submissionSuccessful": "Odeslání úspěšné.",
|
||||
"submit": "Odeslat",
|
||||
@@ -247,20 +251,21 @@
|
||||
"welcome": "Vítejte"
|
||||
},
|
||||
"operators": {
|
||||
"contains": "obsahuje",
|
||||
"equals": "rovná se",
|
||||
"isNotEqualTo": "není rovno",
|
||||
"isIn": "je v",
|
||||
"isNotIn": "není in",
|
||||
"exists": "existuje",
|
||||
"isGreaterThan": "je větší než",
|
||||
"isGreaterThanOrEqualTo": "je větší nebo rovno",
|
||||
"isIn": "je v",
|
||||
"isLessThan": "je menší než",
|
||||
"isLessThanOrEqualTo": "je menší nebo rovno",
|
||||
"isGreaterThanOrEqualTo": "je větší nebo rovno",
|
||||
"near": "blízko",
|
||||
"isLike": "je jako",
|
||||
"contains": "obsahuje"
|
||||
"isNotEqualTo": "není rovno",
|
||||
"isNotIn": "není in",
|
||||
"near": "blízko"
|
||||
},
|
||||
"upload": {
|
||||
"dragAndDrop": "Přetáhněte soubor",
|
||||
"dragAndDropHere": "nebo sem přetáhněte soubor",
|
||||
"fileName": "Název souboru",
|
||||
"fileSize": "Velikost souboru",
|
||||
@@ -269,7 +274,6 @@
|
||||
"moreInfo": "Více informací",
|
||||
"selectCollectionToBrowse": "Vyberte kolekci pro procházení",
|
||||
"selectFile": "Vyberte soubor",
|
||||
"dragAndDrop": "Přetáhněte soubor",
|
||||
"sizes": "Velikosti",
|
||||
"width": "Šířka"
|
||||
},
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Zobrazuji verze pro {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Zobrazuji verze pro globální {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"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.",
|
||||
"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_other": "Die folgenden Felder sind nicht korrekt:",
|
||||
"incorrectCollection": "Falsche Sammlung",
|
||||
"invalidFileType": "Ungültiger Datei-Typ",
|
||||
"invalidFileTypeValue": "Ungültiger Datei-Typ: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Erfolgreich gelöscht.",
|
||||
"deleting": "Lösche...",
|
||||
"descending": "Absteigend",
|
||||
"deselectAllRows": "Alle Zeilen abwählen",
|
||||
"duplicate": "Duplizieren",
|
||||
"duplicateWithoutSaving": "Dupliziere ohne Änderungen zu speichern",
|
||||
"edit": "Bearbeiten",
|
||||
@@ -184,9 +185,9 @@
|
||||
"email": "E-Mail",
|
||||
"emailAddress": "E-Mail-Adresse",
|
||||
"enterAValue": "Gib einen Wert ein",
|
||||
"fallbackToDefaultLocale": "Rückgriff auf das Standardgebietsschema",
|
||||
"error": "Fehler",
|
||||
"errors": "Fehler",
|
||||
"fallbackToDefaultLocale": "Rückgriff auf das Standardgebietsschema",
|
||||
"filter": "Filter",
|
||||
"filterWhere": "Filter {{label}} wo",
|
||||
"filters": "Filter",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Speichert...",
|
||||
"searchBy": "Suche nach {{label}}",
|
||||
"selectAll": "Alle auswählen {{count}} {{label}}",
|
||||
"selectAllRows": "Wählen Sie alle Zeilen aus",
|
||||
"selectValue": "Wert auswählen",
|
||||
"selectedCount": "{{count}} {{label}} ausgewählt",
|
||||
"showAllLabel": "Zeige alle {{label}}",
|
||||
"sorryNotFound": "Entschuldige, es entspricht nichts deiner Anfrage",
|
||||
"sort": "Sortieren",
|
||||
"sortByLabelDirection": "Sortieren nach {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Auf dieser Seite bleiben",
|
||||
"submissionSuccessful": "Einrichung erfolgreich.",
|
||||
"submit": "Senden",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Betrachte Versionen für {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Betrachte Versionen für das Globale Dokument {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Deleted successfully.",
|
||||
"deleting": "Deleting...",
|
||||
"descending": "Descending",
|
||||
"deselectAllRows": "Deselect all rows",
|
||||
"duplicate": "Duplicate",
|
||||
"duplicateWithoutSaving": "Duplicate without saving changes",
|
||||
"edit": "Edit",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Saving...",
|
||||
"searchBy": "Search by {{label}}",
|
||||
"selectAll": "Select all {{count}} {{label}}",
|
||||
"selectAllRows": "Select all rows",
|
||||
"selectValue": "Select a value",
|
||||
"selectedCount": "{{count}} {{label}} selected",
|
||||
"showAllLabel": "Show all {{label}}",
|
||||
"sorryNotFound": "Sorry—there is nothing to correspond with your request.",
|
||||
"sort": "Sort",
|
||||
"sortByLabelDirection": "Sort by {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Stay on this page",
|
||||
"submissionSuccessful": "Submission Successful.",
|
||||
"submit": "Submit",
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"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.",
|
||||
"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_other": "Los siguientes campos son inválidos:",
|
||||
"incorrectCollection": "Colección Incorrecta",
|
||||
"invalidFileType": "Tipo de archivo inválido",
|
||||
"invalidFileTypeValue": "Tipo de archivo inválido: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Borrado exitosamente.",
|
||||
"deleting": "Eliminando...",
|
||||
"descending": "Descendente",
|
||||
"deselectAllRows": "Deselecciona todas las filas",
|
||||
"duplicate": "Duplicar",
|
||||
"duplicateWithoutSaving": "Duplicar sin guardar cambios",
|
||||
"edit": "Editar",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Guardando...",
|
||||
"searchBy": "Buscar por {{label}}",
|
||||
"selectAll": "Seleccionar todo {{count}} {{label}}",
|
||||
"selectAllRows": "Selecciona todas las filas",
|
||||
"selectValue": "Selecciona un valor",
|
||||
"selectedCount": "{{count}} {{label}} seleccionado",
|
||||
"showAllLabel": "Muestra todas {{label}}",
|
||||
"sorryNotFound": "Lo sentimos. No hay nada que corresponda con tu solicitud.",
|
||||
"sort": "Ordenar",
|
||||
"sortByLabelDirection": "Ordenar por {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Permanecer en esta página",
|
||||
"submissionSuccessful": "Envío realizado correctamente.",
|
||||
"submit": "Enviar",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Viendo versiones para {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Viendo versiones para el global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "هنگام حذف فایل خطایی روی داد.",
|
||||
"deletingTitle": "هنگام حذف {{title}} خطایی رخ داد. لطفاً وضعیت اتصال اینترنت خود را بررسی کنید.",
|
||||
"emailOrPasswordIncorrect": "رایانامه یا گذرواژه ارائه شده نادرست است.",
|
||||
"followingFieldsInvalid_other": "کادرهای زیر نامعتبر هستند:",
|
||||
"followingFieldsInvalid_one": "کادر زیر نامعتبر است:",
|
||||
"followingFieldsInvalid_other": "کادرهای زیر نامعتبر هستند:",
|
||||
"incorrectCollection": "مجموعه نادرست",
|
||||
"invalidFileType": "نوع رسانه نامعتبر است",
|
||||
"invalidFileTypeValue": "نوع رسانه نامعتبر: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "با موفقیت حذف شد.",
|
||||
"deleting": "در حال حذف...",
|
||||
"descending": "رو به پایین",
|
||||
"deselectAllRows": "تمام سطرها را از انتخاب خارج کنید",
|
||||
"duplicate": "تکراری",
|
||||
"duplicateWithoutSaving": "رونوشت بدون ذخیره کردن تغییرات",
|
||||
"edit": "نگارش",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "در حال ذخیره...",
|
||||
"searchBy": "جستجو بر اساس {{label}}",
|
||||
"selectAll": "انتخاب همه {{count}} {{label}}",
|
||||
"selectAllRows": "انتخاب تمام سطرها",
|
||||
"selectValue": "یک مقدار را انتخاب کنید",
|
||||
"selectedCount": "{{count}} {{label}} انتخاب شد",
|
||||
"showAllLabel": "نمایش همه {{label}}",
|
||||
"sorryNotFound": "متأسفانه چیزی برای مطابقت با درخواست شما وجود ندارد.",
|
||||
"sort": "مرتبسازی",
|
||||
"sortByLabelDirection": "مرتب کردن بر اساس {{label}} {{direction}}",
|
||||
"stayOnThisPage": "ماندن در این برگه",
|
||||
"submissionSuccessful": "با موفقیت ثبت شد.",
|
||||
"submit": "فرستادن",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "مشاهده نگارشها برای {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "مشاهده نگارشهای کلی {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"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.",
|
||||
"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_other": "Les champs suivants ne sont pas valides :",
|
||||
"incorrectCollection": "Collection incorrecte",
|
||||
"invalidFileType": "Type de fichier invalide",
|
||||
"invalidFileTypeValue": "Type de fichier invalide : {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Supprimé(e) avec succès.",
|
||||
"deleting": "Suppression en cours...",
|
||||
"descending": "Descendant(e)",
|
||||
"deselectAllRows": "Désélectionner toutes les lignes",
|
||||
"duplicate": "Dupliquer",
|
||||
"duplicateWithoutSaving": "Dupliquer sans enregistrer les modifications",
|
||||
"edit": "Éditer",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Sauvegarde en cours...",
|
||||
"searchBy": "Rechercher par {{label}}",
|
||||
"selectAll": "Tout sélectionner {{count}} {{label}}",
|
||||
"selectAllRows": "Sélectionnez toutes les lignes",
|
||||
"selectValue": "Sélectionnez une valeur",
|
||||
"selectedCount": "{{count}} {{label}} sélectionné",
|
||||
"showAllLabel": "Afficher tous les {{label}}",
|
||||
"sorryNotFound": "Désolé, rien ne correspond à votre demande.",
|
||||
"sort": "Trier",
|
||||
"sortByLabelDirection": "Trier par {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Rester sur cette page",
|
||||
"submissionSuccessful": "Soumission réussie.",
|
||||
"submit": "Soumettre",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Affichage des versions de ou du {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Affichage des versions globales de ou du {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"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.",
|
||||
"emailOrPasswordIncorrect": "Email ili lozinka netočni.",
|
||||
"followingFieldsInvalid_other": "Ova polja su nevaljana:",
|
||||
"followingFieldsInvalid_one": " Ovo polje je nevaljano:",
|
||||
"followingFieldsInvalid_other": "Ova polja su nevaljana:",
|
||||
"incorrectCollection": "Nevaljana kolekcija",
|
||||
"invalidFileType": "Nevaljan tip datoteke",
|
||||
"invalidFileTypeValue": "Nevaljan tip datoteke: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Uspješno obrisano.",
|
||||
"deleting": "Brisanje...",
|
||||
"descending": "Silazno",
|
||||
"deselectAllRows": "Odznači sve redove",
|
||||
"duplicate": "Duplikat",
|
||||
"duplicateWithoutSaving": "Dupliciraj bez spremanja promjena",
|
||||
"edit": "Uredi",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Spremanje...",
|
||||
"searchBy": "Traži po {{label}}",
|
||||
"selectAll": "Odaberite sve {{count}} {{label}}",
|
||||
"selectAllRows": "Odaberite sve redove",
|
||||
"selectValue": "Odaberi vrijednost",
|
||||
"selectedCount": "{{count}} {{label}} odabrano",
|
||||
"showAllLabel": "Prikaži sve {{label}}",
|
||||
"sorryNotFound": "Nažalost, ne postoji ništa što odgovara vašem zahtjevu.",
|
||||
"sort": "Sortiraj",
|
||||
"sortByLabelDirection": "Sortiraj prema {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Ostani na ovoj stranici",
|
||||
"submissionSuccessful": "Uspješno slanje",
|
||||
"submit": "Podnesi",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Pregled verzija za {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Pregled verzije za globalni {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"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.",
|
||||
"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_other": "A következő mezők érvénytelenek:",
|
||||
"incorrectCollection": "Helytelen gyűjtemény",
|
||||
"invalidFileType": "Érvénytelen fájltípus",
|
||||
"invalidFileTypeValue": "Érvénytelen fájltípus: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Sikeresen törölve.",
|
||||
"deleting": "Törlés...",
|
||||
"descending": "Csökkenő",
|
||||
"deselectAllRows": "Jelölje ki az összes sort",
|
||||
"duplicate": "Duplikálás",
|
||||
"duplicateWithoutSaving": "Duplikálás a módosítások mentése nélkül",
|
||||
"edit": "Szerkesztés",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Mentés...",
|
||||
"searchBy": "Keresés a következő szerint: {{label}}",
|
||||
"selectAll": "Az összes kijelölése: {{count}} {{label}}",
|
||||
"selectAllRows": "Válassza ki az összes sort",
|
||||
"selectValue": "Válasszon ki egy értéket",
|
||||
"selectedCount": "{{count}} {{label}} kiválasztva",
|
||||
"showAllLabel": "Mutasd az összes {{címke}}",
|
||||
"sorryNotFound": "Sajnáljuk – nincs semmi, ami megfelelne a kérésének.",
|
||||
"sort": "Rendezés",
|
||||
"sortByLabelDirection": "Rendezés {{label}} {{direction}} szerint",
|
||||
"stayOnThisPage": "Maradjon ezen az oldalon",
|
||||
"submissionSuccessful": "Beküldés sikeres.",
|
||||
"submit": "Beküldés",
|
||||
@@ -247,20 +251,21 @@
|
||||
"welcome": "Üdvözöljük"
|
||||
},
|
||||
"operators": {
|
||||
"contains": "tartalmaz",
|
||||
"equals": "egyenlő",
|
||||
"isNotEqualTo": "nem egyenlő",
|
||||
"isIn": "benne van",
|
||||
"isNotIn": "nincs benne",
|
||||
"exists": "létezik",
|
||||
"isGreaterThan": "nagyobb, mint",
|
||||
"isGreaterThanOrEqualTo": "nagyobb vagy egyenlő, mint",
|
||||
"isIn": "benne van",
|
||||
"isLessThan": "kisebb, mint",
|
||||
"isLessThanOrEqualTo": "kisebb vagy egyenlő, mint",
|
||||
"isGreaterThanOrEqualTo": "nagyobb vagy egyenlő, mint",
|
||||
"near": "közel",
|
||||
"isLike": "olyan, mint",
|
||||
"contains": "tartalmaz"
|
||||
"isNotEqualTo": "nem egyenlő",
|
||||
"isNotIn": "nincs benne",
|
||||
"near": "közel"
|
||||
},
|
||||
"upload": {
|
||||
"dragAndDrop": "Húzzon ide egy fájlt",
|
||||
"dragAndDropHere": "vagy húzzon ide egy fájlt",
|
||||
"fileName": "Fájlnév",
|
||||
"fileSize": "Fájl mérete",
|
||||
@@ -269,7 +274,6 @@
|
||||
"moreInfo": "További információ",
|
||||
"selectCollectionToBrowse": "Válassza ki a böngészni kívánt gyűjteményt",
|
||||
"selectFile": "Válasszon ki egy fájlt",
|
||||
"dragAndDrop": "Húzzon ide egy fájlt",
|
||||
"sizes": "Méretek",
|
||||
"width": "Szélesség"
|
||||
},
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "A {{entityLabel}} {{documentTitle}} verzióinak megtekintése",
|
||||
"viewingVersionsGlobal": "A globális {{entityLabel}} verzióinak megtekintése"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"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.",
|
||||
"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_other": "I seguenti campi non sono validi:",
|
||||
"incorrectCollection": "Collezione non corretta",
|
||||
"invalidFileType": "Tipo di file non valido",
|
||||
"invalidFileTypeValue": "Tipo di file non valido: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Eliminato con successo.",
|
||||
"deleting": "Sto eliminando...",
|
||||
"descending": "Decrescente",
|
||||
"deselectAllRows": "Deseleziona tutte le righe",
|
||||
"duplicate": "Duplica",
|
||||
"duplicateWithoutSaving": "Duplica senza salvare le modifiche",
|
||||
"edit": "Modificare",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Salvo...",
|
||||
"searchBy": "Cerca per {{label}}",
|
||||
"selectAll": "Seleziona tutto {{count}} {{label}}",
|
||||
"selectAllRows": "Seleziona tutte le righe",
|
||||
"selectValue": "Seleziona un valore",
|
||||
"selectedCount": "{{count}} {{label}} selezionato",
|
||||
"showAllLabel": "Mostra tutti {{label}}",
|
||||
"sorryNotFound": "Siamo spiacenti, non c'è nulla che corrisponda alla tua richiesta.",
|
||||
"sort": "Ordina",
|
||||
"sortByLabelDirection": "Ordina per {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Rimani su questa pagina",
|
||||
"submissionSuccessful": "Invio riuscito.",
|
||||
"submit": "Invia",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Visualizzazione delle versioni per {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Visualizzazione delle versioni per {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "ファイルの削除中にエラーが発生しました。",
|
||||
"deletingTitle": "{{title}} を削除する際にエラーが発生しました。接続を確認してからもう一度お試しください。",
|
||||
"emailOrPasswordIncorrect": "メールアドレス、または、パスワードが正しくありません。",
|
||||
"followingFieldsInvalid_other": "次のフィールドは無効です:",
|
||||
"followingFieldsInvalid_one": "次のフィールドは無効です:",
|
||||
"followingFieldsInvalid_other": "次のフィールドは無効です:",
|
||||
"incorrectCollection": "不正なコレクション",
|
||||
"invalidFileType": "無効なファイル形式",
|
||||
"invalidFileTypeValue": "無効なファイル形式: {{value}}",
|
||||
@@ -173,14 +173,15 @@
|
||||
"deletedSuccessfully": "正常に削除されました。",
|
||||
"deleting": "削除しています...",
|
||||
"descending": "降順",
|
||||
"deselectAllRows": "すべての行の選択を解除します",
|
||||
"duplicate": "複製",
|
||||
"duplicateWithoutSaving": "変更を保存せずに複製",
|
||||
"edit": "編集",
|
||||
"editLabel": "{{label}} を編集",
|
||||
"editing": "編集",
|
||||
"editingLabel_many": "{{count}}つの{{label}}を編集しています",
|
||||
"editingLabel_one": "{{count}}つの{{label}}を編集しています",
|
||||
"editingLabel_other": "{{count}}つの{{label}}を編集しています",
|
||||
"editing": "編集",
|
||||
"email": "メールアドレス",
|
||||
"emailAddress": "メールアドレス",
|
||||
"enterAValue": "値を入力",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "保存しています...",
|
||||
"searchBy": "{{label}} で検索",
|
||||
"selectAll": "すべての{{count}}つの{{label}}を選択",
|
||||
"selectAllRows": "すべての行を選択します",
|
||||
"selectValue": "値を選択",
|
||||
"selectedCount": "{{count}}つの{{label}}を選択中",
|
||||
"showAllLabel": "すべての{{label}}を表示する",
|
||||
"sorryNotFound": "申し訳ありません。リクエストに対応する内容が見つかりませんでした。",
|
||||
"sort": "並び替え",
|
||||
"sortByLabelDirection": "{{label}}により並べ替え {{direction}}",
|
||||
"stayOnThisPage": "この画面にとどまる",
|
||||
"submissionSuccessful": "送信が成功しました。",
|
||||
"submit": "送信",
|
||||
@@ -247,20 +251,21 @@
|
||||
"welcome": "ようこそ"
|
||||
},
|
||||
"operators": {
|
||||
"contains": "含む",
|
||||
"equals": "等しい",
|
||||
"isNotEqualTo": "等しくない",
|
||||
"isIn": "あります",
|
||||
"isNotIn": "入っていません",
|
||||
"exists": "存在す",
|
||||
"isGreaterThan": "より大きい",
|
||||
"isGreaterThanOrEqualTo": "以上",
|
||||
"isIn": "あります",
|
||||
"isLessThan": "より小さい",
|
||||
"isLessThanOrEqualTo": "以下",
|
||||
"isGreaterThanOrEqualTo": "以上",
|
||||
"near": "近く",
|
||||
"isLike": "のような",
|
||||
"contains": "含む"
|
||||
"isNotEqualTo": "等しくない",
|
||||
"isNotIn": "入っていません",
|
||||
"near": "近く"
|
||||
},
|
||||
"upload": {
|
||||
"dragAndDrop": "ファイルをドラッグ アンド ドロップする",
|
||||
"dragAndDropHere": "または、このエリアにファイルをドラッグ & ドロップ",
|
||||
"fileName": "ファイル名",
|
||||
"fileSize": "ファイル容量",
|
||||
@@ -269,7 +274,6 @@
|
||||
"moreInfo": "詳細を表示",
|
||||
"selectCollectionToBrowse": "閲覧するコレクションを選択",
|
||||
"selectFile": "ファイルを選択",
|
||||
"dragAndDrop": "ファイルをドラッグ アンド ドロップする",
|
||||
"sizes": "容量",
|
||||
"width": "横幅"
|
||||
},
|
||||
@@ -293,15 +297,18 @@
|
||||
"validUploadID": "有効なアップロードIDではありません。"
|
||||
},
|
||||
"version": {
|
||||
"aboutToPublishSelection": "選択中のすべての{{label}}を公開しようとしています。よろしいですか?",
|
||||
"aboutToRestore": "この {{label}} データを {{versionDate}} 時点のバージョンに復元しようとしています。",
|
||||
"aboutToRestoreGlobal": "グローバルな {{label}} データを {{versionDate}} 時点のバージョンに復元しようとしています。",
|
||||
"aboutToRevertToPublished": "このデータの変更を公開時の状態に戻そうとしています。よろしいですか?",
|
||||
"aboutToUnpublish": "このデータを非公開にしようとしています。よろしいですか?",
|
||||
"aboutToUnpublishSelection": "選択したすべての{{label}}の公開を取り消そうとしています。よろしいですか?",
|
||||
"autosave": "自動保存",
|
||||
"autosavedSuccessfully": "自動保存に成功しました。",
|
||||
"autosavedVersion": "自動保存されたバージョン",
|
||||
"changed": "変更済み",
|
||||
"compareVersion": "バージョンを比較:",
|
||||
"confirmPublish": "公開を確認する",
|
||||
"confirmRevertToSaved": "保存された状態に戻す確認",
|
||||
"confirmUnpublish": "非公開の確認",
|
||||
"confirmVersionRestoration": "バージョン復元の確認",
|
||||
@@ -313,6 +320,7 @@
|
||||
"noRowsFound": "{{label}} は未設定です",
|
||||
"preview": "プレビュー",
|
||||
"problemRestoringVersion": "このバージョンの復元に問題がありました。",
|
||||
"publish": "公開する",
|
||||
"publishChanges": "変更内容を公開",
|
||||
"published": "公開済み",
|
||||
"restoreThisVersion": "このバージョンを復元",
|
||||
@@ -324,6 +332,7 @@
|
||||
"selectLocales": "表示するロケールを選択",
|
||||
"selectVersionToCompare": "比較するバージョンを選択",
|
||||
"showLocales": "ロケールを表示:",
|
||||
"showingVersionsFor": "次のバージョンを表示します:",
|
||||
"status": "ステータス",
|
||||
"type": "タイプ",
|
||||
"unpublish": "非公開",
|
||||
@@ -332,6 +341,7 @@
|
||||
"versionCount_many": "{{count}} バージョンがあります",
|
||||
"versionCount_none": "バージョンがありません",
|
||||
"versionCount_one": "{{count}} バージョンがあります",
|
||||
"versionCount_other": "{{count}}バージョンが見つかりました",
|
||||
"versionCreatedOn": "{{version}} 作成日時:",
|
||||
"versionID": "バージョンID",
|
||||
"versions": "バージョン",
|
||||
@@ -340,4 +350,4 @@
|
||||
"viewingVersions": "表示バージョン: {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "表示バージョン: グローバルな {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "ဖိုင်ကိုဖျက်ရာတွင် အမှားအယွင်းရှိနေသည်။",
|
||||
"deletingTitle": "{{title}} ကို ဖျက်ရာတွင် အမှားအယွင်းရှိခဲ့သည်။ သင့် အင်တာနက်လိုင်းအား စစ်ဆေးပြီး ထပ်မံကြို့စားကြည့်ပါ။",
|
||||
"emailOrPasswordIncorrect": "ထည့်သွင်းထားသော အီးမေးလ် သို့မဟုတ် စကားဝှက်သည် မမှန်ပါ။",
|
||||
"followingFieldsInvalid_other": "ထည့်သွင်းထားသော အချက်အလက်များသည် မမှန်ကန်ပါ။",
|
||||
"followingFieldsInvalid_one": "ထည့်သွင်းထားသော အချက်အလက်သည် မမှန်ကန်ပါ။",
|
||||
"followingFieldsInvalid_other": "ထည့်သွင်းထားသော အချက်အလက်များသည် မမှန်ကန်ပါ။",
|
||||
"incorrectCollection": "မှားယွင်းသော စုစည်းမှု",
|
||||
"invalidFileType": "မမှန်ကန်သော ဖိုင်အမျိုးအစား",
|
||||
"invalidFileTypeValue": "မမှန်ကန်သော ဖိုင်အမျိုးအစား: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။",
|
||||
"deleting": "ဖျက်နေဆဲ ...",
|
||||
"descending": "ဆင်းသက်လာသည်။",
|
||||
"deselectAllRows": "အားလုံးကို မရွေးနိုင်ပါ",
|
||||
"duplicate": "ပုံတူပွားမည်။",
|
||||
"duplicateWithoutSaving": "သေချာပါပြီ။",
|
||||
"edit": "တည်းဖြတ်ပါ။",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "သိမ်းနေဆဲ ...",
|
||||
"searchBy": "ရှာဖွေပါ။",
|
||||
"selectAll": "{{count}} {{label}} အားလုံးကို ရွေးပါ",
|
||||
"selectAllRows": "အားလုံးကိုရွေးချယ်ပါ",
|
||||
"selectValue": "တစ်ခုခုကို ရွေးချယ်ပါ။",
|
||||
"selectedCount": "{{count}} {{label}} ကို ရွေးထားသည်။",
|
||||
"showAllLabel": "Tunjukkan semua {{label}}",
|
||||
"sorryNotFound": "ဝမ်းနည်းပါသည်။ သင်ရှာနေတဲ့ဟာ ဒီမှာမရှိပါ။",
|
||||
"sort": "အစဉ်လိုက်",
|
||||
"sortByLabelDirection": "အစဉ်အလိုက် စီမံခန့်ခွဲထားသည် {{label}} {{direction}}",
|
||||
"stayOnThisPage": "ဒီမှာပဲ ဆက်နေမည်။",
|
||||
"submissionSuccessful": "သိမ်းဆည်းမှု အောင်မြင်ပါသည်။",
|
||||
"submit": "သိမ်းဆည်းမည်။",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} အတွက် ဗားရှင်းများကို ကြည့်ရှုခြင်း",
|
||||
"viewingVersionsGlobal": "`ဂလိုဘယ်ဆိုင်ရာ {{entityLabel}} အတွက် ဗားရှင်းများကို ကြည့်ရှုနေသည်"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Det oppstod en feil under sletting av filen.",
|
||||
"deletingTitle": "Det oppstod en feil under sletting av {{title}}. Sjekk tilkoblingen og prøv igjen.",
|
||||
"emailOrPasswordIncorrect": "E-postadressen eller passordet er feil.",
|
||||
"followingFieldsInvalid_other": "Følgende felter er ugyldige:",
|
||||
"followingFieldsInvalid_one": "Følgende felt er ugyldig:",
|
||||
"followingFieldsInvalid_other": "Følgende felter er ugyldige:",
|
||||
"incorrectCollection": "Ugyldig samling",
|
||||
"invalidFileType": "Ugyldig filtype",
|
||||
"invalidFileTypeValue": "Ugyldig filtype: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Slettet.",
|
||||
"deleting": "Sletter...",
|
||||
"descending": "Synkende",
|
||||
"deselectAllRows": "Fjern markeringen fra alle rader",
|
||||
"duplicate": "Dupliser",
|
||||
"duplicateWithoutSaving": "Dupliser uten å lagre endringer",
|
||||
"edit": "Redigere",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Lagrer...",
|
||||
"searchBy": "Søk etter {{label}}",
|
||||
"selectAll": "Velg alle {{count}} {{label}}",
|
||||
"selectAllRows": "Velg alle rader",
|
||||
"selectValue": "Velg en verdi",
|
||||
"selectedCount": "{{count}} {{label}} valgt",
|
||||
"showAllLabel": "Vis alle {{label}}",
|
||||
"sorryNotFound": "Beklager, det er ingenting som samsvarer med forespørselen din.",
|
||||
"sort": "Sortér",
|
||||
"sortByLabelDirection": "Sorter etter {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Bli på denne siden",
|
||||
"submissionSuccessful": "Innsending vellykket.",
|
||||
"submit": "Send inn",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Viser versjoner for {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Viser versjoner for den globale variabelen {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Er is een fout opgetreden bij het verwijderen van dit bestand.",
|
||||
"deletingTitle": "Er is een fout opgetreden tijdens het verwijderen van {{title}}. Controleer uw verbinding en probeer het opnieuw.",
|
||||
"emailOrPasswordIncorrect": "Het opgegeven e-mailadres of wachtwoord is onjuist.",
|
||||
"followingFieldsInvalid_other": "De volgende velden zijn ongeldig:",
|
||||
"followingFieldsInvalid_one": "Het volgende veld is ongeldig:",
|
||||
"followingFieldsInvalid_other": "De volgende velden zijn ongeldig:",
|
||||
"incorrectCollection": "Ongeldige collectie",
|
||||
"invalidFileType": "Ongeldig bestandstype",
|
||||
"invalidFileTypeValue": "Ongeldig bestandstype: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Succesvol verwijderd.",
|
||||
"deleting": "Verwijderen...",
|
||||
"descending": "Aflopend",
|
||||
"deselectAllRows": "Deselecteer alle rijen",
|
||||
"duplicate": "Dupliceren",
|
||||
"duplicateWithoutSaving": "Dupliceren zonder wijzigingen te bewaren",
|
||||
"edit": "Bewerk",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Bewaren...",
|
||||
"searchBy": "Zoeken op {{label}}",
|
||||
"selectAll": "Alles selecteren {{count}} {{label}}",
|
||||
"selectAllRows": "Selecteer alle rijen",
|
||||
"selectValue": "Selecteer een waarde",
|
||||
"selectedCount": "{{count}} {{label}} geselecteerd",
|
||||
"showAllLabel": "Toon alle {{label}}",
|
||||
"sorryNotFound": "Sorry, er is niets dat overeen komt met uw verzoek.",
|
||||
"sort": "Sorteer",
|
||||
"sortByLabelDirection": "Sorteer op {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Blijf op deze pagina",
|
||||
"submissionSuccessful": "Indiening succesvol.",
|
||||
"submit": "Indienen",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Bekijk versies voor {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Bekijk versies voor global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "",
|
||||
"deletingTitle": "Wystąpił błąd podczas usuwania {{title}}. Proszę, sprawdź swoje połączenie i spróbuj ponownie.",
|
||||
"emailOrPasswordIncorrect": "Podany adres e-mail lub hasło jest nieprawidłowe.",
|
||||
"followingFieldsInvalid_other": "Następujące pola są nieprawidłowe:",
|
||||
"followingFieldsInvalid_one": "To pole jest nieprawidłowe:",
|
||||
"followingFieldsInvalid_other": "Następujące pola są nieprawidłowe:",
|
||||
"incorrectCollection": "Nieprawidłowa kolekcja",
|
||||
"invalidFileType": "Nieprawidłowy typ pliku",
|
||||
"invalidFileTypeValue": "Nieprawidłowy typ pliku: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Skutecznie usunięte.",
|
||||
"deleting": "Usuwanie...",
|
||||
"descending": "Malejąco",
|
||||
"deselectAllRows": "Odznacz wszystkie wiersze",
|
||||
"duplicate": "Zduplikuj",
|
||||
"duplicateWithoutSaving": "Zduplikuj bez zapisywania zmian",
|
||||
"edit": "Edytować",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Zapisywanie...",
|
||||
"searchBy": "Szukaj według",
|
||||
"selectAll": "Wybierz wszystkie {{liczba}} {{etykieta}}",
|
||||
"selectAllRows": "Wybierz wszystkie wiersze",
|
||||
"selectValue": "Wybierz wartość",
|
||||
"selectedCount": "Wybrano {{count}} {{label}}",
|
||||
"showAllLabel": "Pokaż wszystkie {{label}}",
|
||||
"sorryNotFound": "Przepraszamy — nie ma nic, co odpowiadałoby twojej prośbie.",
|
||||
"sort": "Sortuj",
|
||||
"sortByLabelDirection": "Sortuj według {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Pozostań na stronie",
|
||||
"submissionSuccessful": "Zgłoszenie zakończone powodzeniem.",
|
||||
"submit": "Zatwierdź",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Przeglądanie wersji {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Przeglądanie wersji dla globalnej kolekcji {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Ocorreu um erro ao excluir o arquivo.",
|
||||
"deletingTitle": "Ocorreu um erro ao excluir {{title}}. Por favor, verifique sua conexão e tente novamente.",
|
||||
"emailOrPasswordIncorrect": "O email ou senha fornecido está incorreto.",
|
||||
"followingFieldsInvalid_other": "Os campos a seguir estão inválidos:",
|
||||
"followingFieldsInvalid_one": "O campo a seguir está inválido:",
|
||||
"followingFieldsInvalid_other": "Os campos a seguir estão inválidos:",
|
||||
"incorrectCollection": "Coleção Incorreta",
|
||||
"invalidFileType": "Tipo de arquivo inválido",
|
||||
"invalidFileTypeValue": "Tipo de arquivo inválido: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Apagado com sucesso.",
|
||||
"deleting": "Excluindo...",
|
||||
"descending": "Decrescente",
|
||||
"deselectAllRows": "Desmarcar todas as linhas",
|
||||
"duplicate": "Duplicar",
|
||||
"duplicateWithoutSaving": "Duplicar sem salvar alterações",
|
||||
"edit": "Editar",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Salvando...",
|
||||
"searchBy": "Buscar por {{label}}",
|
||||
"selectAll": "Selecione tudo {{count}} {{label}}",
|
||||
"selectAllRows": "Selecione todas as linhas",
|
||||
"selectValue": "Selecione um valor",
|
||||
"selectedCount": "{{count}} {{label}} selecionado",
|
||||
"showAllLabel": "Mostre todos {{label}}",
|
||||
"sorryNotFound": "Desculpe—não há nada que corresponda à sua requisição.",
|
||||
"sort": "Ordenar",
|
||||
"sortByLabelDirection": "Ordenar por {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Permanecer nessa página",
|
||||
"submissionSuccessful": "Envio bem-sucedido.",
|
||||
"submit": "Enviar",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Visualizando versões para o/a {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Visualizando versões para o global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "S-a produs o eroare la ștergerea fișierului.",
|
||||
"deletingTitle": "S-a produs o eroare în timpul ștergerii {{title}}. Vă rugăm să verificați conexiunea și să încercați din nou.",
|
||||
"emailOrPasswordIncorrect": "Adresa de e-mail sau parola este incorectă.",
|
||||
"followingFieldsInvalid_other": "Următoarele câmpuri nu sunt valabile:",
|
||||
"followingFieldsInvalid_one": "Următorul câmp nu este valid:",
|
||||
"followingFieldsInvalid_other": "Următoarele câmpuri nu sunt valabile:",
|
||||
"incorrectCollection": "Colecție incorectă",
|
||||
"invalidFileType": "Tip de fișier invalid",
|
||||
"invalidFileTypeValue": "Tip de fișier invalid: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Șters cu succes.",
|
||||
"deleting": "Deleting...",
|
||||
"descending": "Descendentă",
|
||||
"deselectAllRows": "Deselectează toate rândurile",
|
||||
"duplicate": "Duplicați",
|
||||
"duplicateWithoutSaving": "Duplicați fără salvarea modificărilor",
|
||||
"edit": "Editează",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Salvare...",
|
||||
"searchBy": "Căutați după {{label}}",
|
||||
"selectAll": "Selectați toate {{count}} {{label}}",
|
||||
"selectAllRows": "Selectează toate rândurile",
|
||||
"selectValue": "Selectați o valoare",
|
||||
"selectedCount": "{{count}} {{label}} selectate",
|
||||
"showAllLabel": "Afișează toate {{eticheta}}",
|
||||
"sorryNotFound": "Ne pare rău - nu există nimic care să corespundă cu cererea dvs.",
|
||||
"sort": "Sortează",
|
||||
"sortByLabelDirection": "Sortează după {{etichetă}} {{direcţie}}",
|
||||
"stayOnThisPage": "Rămâneți pe această pagină",
|
||||
"submissionSuccessful": "Trimitere cu succes.",
|
||||
"submit": "Trimite",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Vizualizarea versiunilor pentru {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Vizualizarea versiunilor pentru globala {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Произошла ошибка при удалении файла.",
|
||||
"deletingTitle": "При удалении {{title}} произошла ошибка. Пожалуйста, проверьте соединение и повторите попытку.",
|
||||
"emailOrPasswordIncorrect": "Указанный email или пароль неверен.",
|
||||
"followingFieldsInvalid_other": "Следующие поля недействительны:",
|
||||
"followingFieldsInvalid_one": "Следующее поле недействительно:",
|
||||
"followingFieldsInvalid_other": "Следующие поля недействительны:",
|
||||
"incorrectCollection": "Неправильная Коллекция",
|
||||
"invalidFileType": "Недопустимый тип файла",
|
||||
"invalidFileTypeValue": "Недопустимый тип файла: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Удален успешно.",
|
||||
"deleting": "Удаление...",
|
||||
"descending": "Уменьшение",
|
||||
"deselectAllRows": "Снять выделение со всех строк",
|
||||
"duplicate": "Дублировать",
|
||||
"duplicateWithoutSaving": "Дублирование без сохранения изменений",
|
||||
"edit": "Редактировать",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Сохранение...",
|
||||
"searchBy": "Искать по",
|
||||
"selectAll": "Выбрать все {{count}} {{label}}",
|
||||
"selectAllRows": "Выбрать все строки",
|
||||
"selectValue": "Выбрать значение",
|
||||
"selectedCount": "{{count}} {{label}} выбрано",
|
||||
"showAllLabel": "Показать все {{label}}",
|
||||
"sorryNotFound": "К сожалению, ничего подходящего под ваш запрос нет.",
|
||||
"sort": "Сортировать",
|
||||
"sortByLabelDirection": "Сортировать по {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Остаться на этой странице",
|
||||
"submissionSuccessful": "Успешно отправлено.",
|
||||
"submit": "Отправить",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Просмотр версий для {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Просмотр версии для глобальной Коллекции {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Det gick inte att ta bort filen.",
|
||||
"deletingTitle": "Det uppstod ett fel vid borttagningen av {{title}}. Vänligen kontrollera din anslutning och försök igen.",
|
||||
"emailOrPasswordIncorrect": "E-postadressen eller lösenordet som angivits är felaktigt.",
|
||||
"followingFieldsInvalid_other": "Följande fält är ogiltiga:",
|
||||
"followingFieldsInvalid_one": "Följande fält är ogiltigt:",
|
||||
"followingFieldsInvalid_other": "Följande fält är ogiltiga:",
|
||||
"incorrectCollection": "Felaktig Samling",
|
||||
"invalidFileType": "Ogiltig filtyp",
|
||||
"invalidFileTypeValue": "Ogiltig filtyp: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Togs bort framgångsrikt.",
|
||||
"deleting": "Tar bort...",
|
||||
"descending": "Fallande",
|
||||
"deselectAllRows": "Avmarkera alla rader",
|
||||
"duplicate": "Duplicera",
|
||||
"duplicateWithoutSaving": "Duplicera utan att spara ändringar",
|
||||
"edit": "Redigera",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Sparar...",
|
||||
"searchBy": "Sök efter {{label}}",
|
||||
"selectAll": "Välj alla {{count}} {{label}}",
|
||||
"selectAllRows": "Välj alla rader",
|
||||
"selectValue": "Välj ett värde",
|
||||
"selectedCount": "{{count}} {{label}} har valts",
|
||||
"showAllLabel": "Visa alla {{label}}",
|
||||
"sorryNotFound": "Tyvärr–det finns inget som motsvarar din begäran.",
|
||||
"sort": "Sortera",
|
||||
"sortByLabelDirection": "Sortera efter {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Stanna på denna sida",
|
||||
"submissionSuccessful": "Inlämningen Lyckades.",
|
||||
"submit": "Lämna in",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Visar versioner för {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Visa versioner för den globala {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "เกิดปัญหาระหว่างการลบไฟล์",
|
||||
"deletingTitle": "เกิดปัญหาระหว่างการลบ {{title}} โปรดตรวจสอบการเชื่อมต่อของคุณแล้วลองอีกครั้ง",
|
||||
"emailOrPasswordIncorrect": "อีเมลหรือรหัสผ่านไม่ถูกต้อง",
|
||||
"followingFieldsInvalid_other": "ช่องต่อไปนี้ไม่ถูกต้อง:",
|
||||
"followingFieldsInvalid_one": "ช่องต่อไปนี้ไม่ถูกต้อง:",
|
||||
"followingFieldsInvalid_other": "ช่องต่อไปนี้ไม่ถูกต้อง:",
|
||||
"incorrectCollection": "Collection ไม่ถูกต้อง",
|
||||
"invalidFileType": "ประเภทของไฟล์ไม่ถูกต้อง",
|
||||
"invalidFileTypeValue": "ประเภทของไฟล์ไม่ถูกต้อง: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "ลบสำเร็จ",
|
||||
"deleting": "กำลังลบ...",
|
||||
"descending": "มากไปน้อย",
|
||||
"deselectAllRows": "ยกเลิกการเลือกทุกแถว",
|
||||
"duplicate": "สำเนา",
|
||||
"duplicateWithoutSaving": "สำเนาโดยไม่บันทึกการแก้ไข",
|
||||
"edit": "แก้ไข",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "กำลังบันทึก...",
|
||||
"searchBy": "ค้นหาด้วย {{label}}",
|
||||
"selectAll": "เลือกทั้งหมด {{count}} {{label}}",
|
||||
"selectAllRows": "เลือกทุกแถว",
|
||||
"selectValue": "เลือกค่า",
|
||||
"selectedCount": "เลือก {{count}} {{label}} แล้ว",
|
||||
"showAllLabel": "แสดง {{label}} ทั้งหมด",
|
||||
"sorryNotFound": "ขออภัย ไม่สามารถทำตามคำขอของคุณได้",
|
||||
"sort": "เรียง",
|
||||
"sortByLabelDirection": "เรียงลำดับตาม {{label}} {{direction}}",
|
||||
"stayOnThisPage": "อยู่หน้านี้ต่อ",
|
||||
"submissionSuccessful": "ส่งสำเร็จ",
|
||||
"submit": "ส่ง",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "กำลังดูเวอร์ชันของ {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "กำลังดูเวอร์ชันของ global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Dosya silinirken bir hatayla karşılaşıldı.",
|
||||
"deletingTitle": "{{title}} silinirken bir sorun yaşandı. Lütfen internet bağlantınızı kontrol edip tekrar deneyin.",
|
||||
"emailOrPasswordIncorrect": "Girilen e-posta veya parola hatalı",
|
||||
"followingFieldsInvalid_other": "Lütfen geçersiz alanları düzeltin:",
|
||||
"followingFieldsInvalid_one": "Lütfen geçersiz alanı düzeltin:",
|
||||
"followingFieldsInvalid_other": "Lütfen geçersiz alanları düzeltin:",
|
||||
"incorrectCollection": "Hatalı koleksiyon",
|
||||
"invalidFileType": "Geçersiz dosya türü",
|
||||
"invalidFileTypeValue": "Geçersiz dosya türü: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Başarıyla silindi.",
|
||||
"deleting": "Siliniyor...",
|
||||
"descending": "Azalan",
|
||||
"deselectAllRows": "Tüm satırların seçimini kaldır",
|
||||
"duplicate": "Çoğalt",
|
||||
"duplicateWithoutSaving": "Ayarları kaydetmeden çoğalt",
|
||||
"edit": "Düzenle",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Kaydediliyor...",
|
||||
"searchBy": "Şuna göre sırala: {{label}}",
|
||||
"selectAll": "Tüm {{count}} {{label}}'ı seçin",
|
||||
"selectAllRows": "Tüm satırları seçin",
|
||||
"selectValue": "Bir değer seçin",
|
||||
"selectedCount": "{{count}} {{label}} seçildi",
|
||||
"showAllLabel": "Tüm {{label}} göster",
|
||||
"sorryNotFound": "Üzgünüz, isteğinizle eşleşen bir sonuç bulunamadı.",
|
||||
"sort": "Sırala",
|
||||
"sortByLabelDirection": "{{label}} göre sırala {{direction}}",
|
||||
"stayOnThisPage": "Bu sayfada kal",
|
||||
"submissionSuccessful": "Gönderme başarılı",
|
||||
"submit": "Gönder",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} için sürümler gösteriliyor",
|
||||
"viewingVersionsGlobal": "`Global {{entityLabel}} için sürümler gösteriliyor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,6 +663,9 @@
|
||||
"descending": {
|
||||
"type": "string"
|
||||
},
|
||||
"deselectAllRows": {
|
||||
"type": "string"
|
||||
},
|
||||
"duplicate": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -810,18 +813,27 @@
|
||||
"selectAll": {
|
||||
"type": "string"
|
||||
},
|
||||
"selectAllRows": {
|
||||
"type": "string"
|
||||
},
|
||||
"selectValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"selectedCount": {
|
||||
"type": "string"
|
||||
},
|
||||
"showAllLabel": {
|
||||
"type": "string"
|
||||
},
|
||||
"sorryNotFound": {
|
||||
"type": "string"
|
||||
},
|
||||
"sort": {
|
||||
"type": "string"
|
||||
},
|
||||
"sortByLabelDirection": {
|
||||
"type": "string"
|
||||
},
|
||||
"stayOnThisPage": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Виникла помилка під час видалення файлу",
|
||||
"deletingTitle": "Виникла помилка під час видалення {{title}}, Будь ласка, перевірте ваше з'єднання та спробуйте ще раз.",
|
||||
"emailOrPasswordIncorrect": "Вказаний email або пароль не є вірними",
|
||||
"followingFieldsInvalid_other": "Наступні поля не є вірними",
|
||||
"followingFieldsInvalid_one": "Наступне поле не є вірним:",
|
||||
"followingFieldsInvalid_other": "Наступні поля не є вірними",
|
||||
"incorrectCollection": "Неправильна колекція",
|
||||
"invalidFileType": "Невіртий тип файлу",
|
||||
"invalidFileTypeValue": "Невірний тип файлу: {{value}}",
|
||||
@@ -131,8 +131,8 @@
|
||||
"selectExistingLabel": "Вибрати існуючий {{label}}",
|
||||
"selectFieldsToEdit": "Виберіть поля для редагування",
|
||||
"showAll": "Показати все",
|
||||
"swapUpload": "Замінити завантаження",
|
||||
"swapRelationship": "Замінити зв'язок",
|
||||
"swapUpload": "Замінити завантаження",
|
||||
"textToDisplay": "Текст для відображення",
|
||||
"toggleBlock": "Перемкнути блок",
|
||||
"uploadNewLabel": "Завантажити новий {{label}}"
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Успішно видалено.",
|
||||
"deleting": "Видалення...",
|
||||
"descending": "В порядку спадання",
|
||||
"deselectAllRows": "Скасувати вибір всіх рядків",
|
||||
"duplicate": "Дублювати",
|
||||
"duplicateWithoutSaving": "Дублювання без збереження змін",
|
||||
"edit": "Редагувати",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Збереження...",
|
||||
"searchBy": "Шукати по {{label}}",
|
||||
"selectAll": "Вибрати всі {{count}} {{label}}",
|
||||
"selectAllRows": "Вибрати всі рядки",
|
||||
"selectValue": "Вибрати значення",
|
||||
"selectedCount": "Вибрано {{count}} {{label}}",
|
||||
"showAllLabel": "Показати всі {{label}}",
|
||||
"sorryNotFound": "Вибачте - немає нічого, що відповідало б Вашому запиту.",
|
||||
"sort": "Сортувати",
|
||||
"sortByLabelDirection": "Сортувати за {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Залишитись на цій сторінці",
|
||||
"submissionSuccessful": "Успішно відправлено.",
|
||||
"submit": "Відправити",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Огляд версій для {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Огляд версій для глобальної колекції {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "Lỗi - Đã xảy ra vấn đề khi xóa tệp này.",
|
||||
"deletingTitle": "Lỗi - Đã xảy ra vấn đề khi xóa {{title}}. Hãy kiểm tra kết nối mạng và thử lại.",
|
||||
"emailOrPasswordIncorrect": "Lỗi - Email hoặc mật khẩu không chính xác.",
|
||||
"followingFieldsInvalid_other": "Lỗi - Những fields sau không hợp lệ:",
|
||||
"followingFieldsInvalid_one": "Lỗi - Field sau không hợp lệ:",
|
||||
"followingFieldsInvalid_other": "Lỗi - Những fields sau không hợp lệ:",
|
||||
"incorrectCollection": "Lỗi - Collection không hợp lệ.",
|
||||
"invalidFileType": "Lỗi - Định dạng tệp không hợp lệ.",
|
||||
"invalidFileTypeValue": "Lỗi - Định dạng tệp không hợp lệ: {{value}}.",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "Đã xoá thành công.",
|
||||
"deleting": "Đang xóa...",
|
||||
"descending": "Xếp theo thứ tự giảm dần",
|
||||
"deselectAllRows": "Bỏ chọn tất cả các hàng",
|
||||
"duplicate": "Tạo bản sao",
|
||||
"duplicateWithoutSaving": "Không lưu dữ liệu và tạo bản sao",
|
||||
"edit": "Chỉnh sửa",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "Đang lưu...",
|
||||
"searchBy": "Tìm với {{label}}",
|
||||
"selectAll": "Chọn tất cả {{count}} {{label}}",
|
||||
"selectAllRows": "Chọn tất cả các hàng",
|
||||
"selectValue": "Chọn một giá trị",
|
||||
"selectedCount": "Đã chọn {{count}} {{label}}",
|
||||
"showAllLabel": "Hiển thị tất cả {{label}}",
|
||||
"sorryNotFound": "Xin lỗi, không có kết quả nào tương ứng với request của bạn.",
|
||||
"sort": "Sắp xếp",
|
||||
"sortByLabelDirection": "Sắp xếp theo {{label}} {{direction}}",
|
||||
"stayOnThisPage": "Ở lại trang này",
|
||||
"submissionSuccessful": "Gửi thành công.",
|
||||
"submit": "Gửi",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "Xem những phiên bản của {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Xem những phiên bản toàn thể (global) của {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"deletingFile": "删除文件时出现了错误。",
|
||||
"deletingTitle": "删除{{title}}时出现了错误。请检查您的连接并重试。",
|
||||
"emailOrPasswordIncorrect": "提供的电子邮件或密码不正确。",
|
||||
"followingFieldsInvalid_other": "以下字段是无效的:",
|
||||
"followingFieldsInvalid_one": "下面的字段是无效的:",
|
||||
"followingFieldsInvalid_other": "以下字段是无效的:",
|
||||
"incorrectCollection": "不正确的集合",
|
||||
"invalidFileType": "无效的文件类型",
|
||||
"invalidFileTypeValue": "无效的文件类型: {{value}}",
|
||||
@@ -173,6 +173,7 @@
|
||||
"deletedSuccessfully": "已成功删除。",
|
||||
"deleting": "删除中...",
|
||||
"descending": "降序",
|
||||
"deselectAllRows": "取消选择所有行",
|
||||
"duplicate": "重复",
|
||||
"duplicateWithoutSaving": "重复而不保存更改。",
|
||||
"edit": "编辑",
|
||||
@@ -222,10 +223,13 @@
|
||||
"saving": "保存中...",
|
||||
"searchBy": "搜索{{label}}",
|
||||
"selectAll": "选择所有 {{count}} {{label}}",
|
||||
"selectAllRows": "选择所有行",
|
||||
"selectValue": "选择一个值",
|
||||
"selectedCount": "已选择 {{count}} {{label}}",
|
||||
"showAllLabel": "显示所有{{label}}",
|
||||
"sorryNotFound": "对不起,没有与您的请求相对应的东西。",
|
||||
"sort": "排序",
|
||||
"sortByLabelDirection": "按{{label}} {{direction}}排序",
|
||||
"stayOnThisPage": "停留在此页面",
|
||||
"submissionSuccessful": "提交成功。",
|
||||
"submit": "提交",
|
||||
@@ -346,4 +350,4 @@
|
||||
"viewingVersions": "正在查看{{entityLabel}} {{documentTitle}}的版本",
|
||||
"viewingVersionsGlobal": "正在查看全局{{entityLabel}}的版本"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/types/constants.ts
Normal file
15
src/types/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const validOperators = [
|
||||
'equals',
|
||||
'contains',
|
||||
'not_equals',
|
||||
'in',
|
||||
'all',
|
||||
'not_in',
|
||||
'exists',
|
||||
'greater_than',
|
||||
'greater_than_equal',
|
||||
'less_than',
|
||||
'less_than_equal',
|
||||
'like',
|
||||
'near',
|
||||
] as const;
|
||||
@@ -1,23 +1,11 @@
|
||||
import { Document as MongooseDocument } from 'mongoose';
|
||||
import { TypeWithTimestamps } from '../collections/config/types';
|
||||
import { FileData } from '../uploads/types';
|
||||
import { validOperators } from './constants';
|
||||
|
||||
export { PayloadRequest } from '../express/types';
|
||||
|
||||
export type Operator =
|
||||
| 'equals'
|
||||
| 'contains'
|
||||
| 'not_equals'
|
||||
| 'in'
|
||||
| 'all'
|
||||
| 'not_in'
|
||||
| 'exists'
|
||||
| 'greater_than'
|
||||
| 'greater_than_equal'
|
||||
| 'less_than'
|
||||
| 'less_than_equal'
|
||||
| 'like'
|
||||
| 'near';
|
||||
export type Operator = typeof validOperators[number];
|
||||
|
||||
export type WhereField = {
|
||||
[key in Operator]?: unknown;
|
||||
|
||||
@@ -115,6 +115,7 @@ export const generateFileData = async <T>({
|
||||
fileBuffer = await sharpFile.toBuffer({ resolveWithObject: true });
|
||||
({ mime, ext } = await fromBuffer(fileBuffer.data)); // This is getting an incorrect gif height back.
|
||||
fileData.width = fileBuffer.info.width;
|
||||
fileData.height = fileBuffer.info.height;
|
||||
fileData.filesize = fileBuffer.info.size;
|
||||
|
||||
// Animated GIFs + WebP aggregate the height from every frame, so we need to use divide by number of pages
|
||||
|
||||
@@ -106,7 +106,7 @@ const createResult = (
|
||||
* @returns true if the image needs to be resized, false otherwise
|
||||
*/
|
||||
const needsResize = (
|
||||
{ width: desiredWidth, height: desiredHeigth, withoutEnlargement, withoutReduction }: ImageSize,
|
||||
{ width: desiredWidth, height: desiredHeight, withoutEnlargement, withoutReduction }: ImageSize,
|
||||
original: ProbedImageSize,
|
||||
): boolean => {
|
||||
// allow enlargement or prevent reduction (our default is to prevent
|
||||
@@ -115,7 +115,8 @@ const needsResize = (
|
||||
return true; // needs resize
|
||||
}
|
||||
|
||||
const isWidthOrHeightNotDefined = !desiredHeigth || !desiredWidth;
|
||||
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth;
|
||||
|
||||
if (isWidthOrHeightNotDefined) {
|
||||
// If with and height are not defined, it means there is a format conversion
|
||||
// and the image needs to be "resized" (transformed).
|
||||
@@ -123,7 +124,7 @@ const needsResize = (
|
||||
}
|
||||
|
||||
const hasInsufficientWidth = original.width < desiredWidth;
|
||||
const hasInsufficientHeight = original.height < desiredHeigth;
|
||||
const hasInsufficientHeight = original.height < desiredHeight;
|
||||
if (hasInsufficientWidth && hasInsufficientHeight) {
|
||||
// doesn't need resize - prevent enlargement. This should only happen if both width and height are insufficient.
|
||||
// if only one dimension is insufficient and the other is sufficient, resizing needs to happen, as the image
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('access control', () => {
|
||||
const duplicateAction = page.locator('.collection-edit__collection-actions >> li').last();
|
||||
await expect(duplicateAction).toContainText('Duplicate');
|
||||
|
||||
await page.locator('#field-approvedForRemoval + button').click();
|
||||
await page.locator('#field-approvedForRemoval').check();
|
||||
await page.locator('#action-save').click();
|
||||
|
||||
const deleteAction = page.locator('.collection-edit__collection-actions >> li').last();
|
||||
|
||||
@@ -198,7 +198,7 @@ describe('admin', () => {
|
||||
|
||||
await page.goto(url.list);
|
||||
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('input#select-all').check();
|
||||
|
||||
await page.locator('.delete-documents__toggle').click();
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('admin', () => {
|
||||
const bulkTitle = 'Bulk update title';
|
||||
await page.goto(url.list);
|
||||
|
||||
await page.locator('.select-all__input').click();
|
||||
await page.locator('input#select-all').check();
|
||||
await page.locator('.edit-many__toggle').click();
|
||||
await page.locator('.field-select .rs__control').click();
|
||||
const options = page.locator('.rs__option');
|
||||
@@ -394,6 +394,25 @@ describe('admin', () => {
|
||||
await page.locator('.condition__actions-remove').click();
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should accept where query from valid URL where parameter', async () => {
|
||||
await createPost({ title: 'post1' });
|
||||
await createPost({ title: 'post2' });
|
||||
await page.goto(`${url.list}?limit=10&page=1&where[or][0][and][0][title][equals]=post1`);
|
||||
|
||||
await expect(page.locator('.react-select--single-value').first()).toContainText('Title en');
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should accept transformed where query from invalid URL where parameter', async () => {
|
||||
await createPost({ title: 'post1' });
|
||||
await createPost({ title: 'post2' });
|
||||
// [title][equals]=post1 should be getting transformed into a valid where[or][0][and][0][title][equals]=post1
|
||||
await page.goto(`${url.list}?limit=10&page=1&where[title][equals]=post1`);
|
||||
|
||||
await expect(page.locator('.react-select--single-value').first()).toContainText('Title en');
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('table columns', () => {
|
||||
@@ -523,18 +542,18 @@ describe('admin', () => {
|
||||
});
|
||||
|
||||
test('should select multiple rows', async () => {
|
||||
const selectAll = page.locator('.select-all');
|
||||
await page.locator('.row-1 .select-row button').click();
|
||||
const selectAll = page.locator('.custom-checkbox:has(#select-all)');
|
||||
await page.locator('.row-1 .cell-_select input').check();
|
||||
|
||||
const indeterminateSelectAll = selectAll.locator('.icon--line');
|
||||
const indeterminateSelectAll = selectAll.locator('.custom-checkbox__icon.partial');
|
||||
expect(indeterminateSelectAll).toBeDefined();
|
||||
|
||||
await selectAll.locator('button').click();
|
||||
const emptySelectAll = selectAll.locator('.icon');
|
||||
await selectAll.locator('input').click();
|
||||
const emptySelectAll = selectAll.locator('.custom-checkbox__icon:not(.check):not(.partial)');
|
||||
await expect(emptySelectAll).toHaveCount(0);
|
||||
|
||||
await selectAll.locator('button').click();
|
||||
const checkSelectAll = selectAll.locator('.icon .icon--check');
|
||||
await selectAll.locator('input').click();
|
||||
const checkSelectAll = selectAll.locator('.custom-checkbox__icon.check');
|
||||
expect(checkSelectAll).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -542,16 +561,16 @@ describe('admin', () => {
|
||||
// delete should not appear without selection
|
||||
await expect(page.locator('#confirm-delete')).toHaveCount(0);
|
||||
// select one row
|
||||
await page.locator('.row-1 .select-row button').click();
|
||||
await page.locator('.row-1 .cell-_select input').check();
|
||||
|
||||
// delete button should be present
|
||||
await expect(page.locator('#confirm-delete')).toHaveCount(1);
|
||||
|
||||
await page.locator('.row-2 .select-row button').click();
|
||||
await page.locator('.row-2 .cell-_select input').check();
|
||||
|
||||
await page.locator('.delete-documents__toggle').click();
|
||||
await page.locator('#confirm-delete').click();
|
||||
await expect(page.locator('.select-row')).toHaveCount(1);
|
||||
await expect(await page.locator('.cell-_select')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -57,6 +57,26 @@ const TextFields: CollectionConfig = {
|
||||
type: 'text',
|
||||
maxLength: 50000,
|
||||
},
|
||||
{
|
||||
name: 'fieldWithDefaultValue',
|
||||
type: 'text',
|
||||
defaultValue: async () => {
|
||||
const defaultValue = new Promise((resolve) => setTimeout(() => resolve('some-value'), 1000));
|
||||
|
||||
return defaultValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dependentOnFieldWithDefaultValue',
|
||||
type: 'text',
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data }) => {
|
||||
return data?.fieldWithDefaultValue || '';
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,15 @@ describe('Fields', () => {
|
||||
expect(doc.defaultFunction).toEqual(defaultText);
|
||||
expect(doc.defaultAsync).toEqual(defaultText);
|
||||
});
|
||||
|
||||
it('should populate default values in beforeValidate hook', async () => {
|
||||
const { fieldWithDefaultValue, dependentOnFieldWithDefaultValue } = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text },
|
||||
});
|
||||
|
||||
await expect(fieldWithDefaultValue).toEqual(dependentOnFieldWithDefaultValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timestamps', () => {
|
||||
|
||||
71
test/hooks/collections/AfterOperation/index.ts
Normal file
71
test/hooks/collections/AfterOperation/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { AfterOperationHook, CollectionConfig } from '../../../../src/collections/config/types';
|
||||
import { AfterOperation } from '../../payload-types';
|
||||
|
||||
export const afterOperationSlug = 'afterOperation';
|
||||
|
||||
const AfterOperation: CollectionConfig = {
|
||||
slug: afterOperationSlug,
|
||||
hooks: {
|
||||
// beforeRead: [(operation) => operation.doc],
|
||||
afterOperation: [
|
||||
async ({ result, operation }) => {
|
||||
if (operation === 'create') {
|
||||
if ('docs' in result) {
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs?.map((doc) => ({
|
||||
...doc,
|
||||
title: 'Title created',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return { ...result, title: 'Title created' };
|
||||
}
|
||||
|
||||
if (operation === 'find') {
|
||||
// only modify the first doc for `find` operations
|
||||
// this is so we can test against the other operations
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs?.map((doc, index) => (index === 0 ? {
|
||||
...doc,
|
||||
title: 'Title read',
|
||||
} : doc)),
|
||||
};
|
||||
}
|
||||
|
||||
if (operation === 'findByID') {
|
||||
return { ...result, title: 'Title read' };
|
||||
}
|
||||
|
||||
if (operation === 'update') {
|
||||
if ('docs' in result) {
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs?.map((doc) => ({
|
||||
...doc,
|
||||
title: 'Title updated',
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'updateByID') {
|
||||
return { ...result, title: 'Title updated' };
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
] as AfterOperationHook<AfterOperation>[],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default AfterOperation;
|
||||
@@ -4,11 +4,13 @@ import Hooks, { hooksSlug } from './collections/Hook';
|
||||
import NestedAfterReadHooks from './collections/NestedAfterReadHooks';
|
||||
import ChainingHooks from './collections/ChainingHooks';
|
||||
import Relations from './collections/Relations';
|
||||
import AfterOperation from './collections/AfterOperation';
|
||||
import Users, { seedHooksUsers } from './collections/Users';
|
||||
import ContextHooks from './collections/ContextHooks';
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
AfterOperation,
|
||||
ContextHooks,
|
||||
TransformHooks,
|
||||
Hooks,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import config from './config';
|
||||
import configPromise from './config';
|
||||
import payload from '../../src';
|
||||
import { RESTClient } from '../helpers/rest';
|
||||
import { transformSlug } from './collections/Transform';
|
||||
@@ -8,10 +8,10 @@ import { hooksSlug } from './collections/Hook';
|
||||
import { chainingHooksSlug } from './collections/ChainingHooks';
|
||||
import { generatedAfterReadText, nestedAfterReadHooksSlug } from './collections/NestedAfterReadHooks';
|
||||
import { relationsSlug } from './collections/Relations';
|
||||
import type { NestedAfterReadHook } from './payload-types';
|
||||
import { hooksUsersSlug } from './collections/Users';
|
||||
import { devUser, regularUser } from '../credentials';
|
||||
import { AuthenticationError } from '../../src/errors';
|
||||
import { afterOperationSlug } from './collections/AfterOperation';
|
||||
import { contextHooksSlug } from './collections/ContextHooks';
|
||||
|
||||
let client: RESTClient;
|
||||
@@ -20,6 +20,7 @@ let apiUrl;
|
||||
describe('Hooks', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
const config = await configPromise;
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: transformSlug });
|
||||
apiUrl = `${serverURL}/api`;
|
||||
});
|
||||
@@ -74,7 +75,7 @@ describe('Hooks', () => {
|
||||
});
|
||||
|
||||
it('should save data generated with afterRead hooks in nested field structures', async () => {
|
||||
const document = await payload.create<NestedAfterReadHook>({
|
||||
const document = await payload.create({
|
||||
collection: nestedAfterReadHooksSlug,
|
||||
data: {
|
||||
text: 'ok',
|
||||
@@ -155,6 +156,62 @@ describe('Hooks', () => {
|
||||
expect(retrievedDocs[0].text).toEqual('ok!!');
|
||||
});
|
||||
|
||||
it('should execute collection afterOperation hook', async () => {
|
||||
const [doc1, doc2] = await Promise.all([
|
||||
await payload.create({
|
||||
collection: afterOperationSlug,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
await payload.create({
|
||||
collection: afterOperationSlug,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(doc1.title === 'Title created').toBeTruthy();
|
||||
expect(doc2.title === 'Title created').toBeTruthy();
|
||||
|
||||
const findResult = await payload.find({
|
||||
collection: afterOperationSlug,
|
||||
});
|
||||
|
||||
expect(findResult.docs).toHaveLength(2);
|
||||
expect(findResult.docs[0].title === 'Title read').toBeTruthy();
|
||||
expect(findResult.docs[1].title === 'Title').toBeTruthy();
|
||||
|
||||
const [updatedDoc1, updatedDoc2] = await Promise.all([
|
||||
await payload.update({
|
||||
collection: afterOperationSlug,
|
||||
id: doc1.id,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
await payload.update({
|
||||
collection: afterOperationSlug,
|
||||
id: doc2.id,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(updatedDoc1.title === 'Title updated').toBeTruthy();
|
||||
expect(updatedDoc2.title === 'Title updated').toBeTruthy();
|
||||
|
||||
const findResult2 = await payload.find({
|
||||
collection: afterOperationSlug,
|
||||
});
|
||||
|
||||
expect(findResult2.docs).toHaveLength(2);
|
||||
expect(findResult2.docs[0].title === 'Title read').toBeTruthy();
|
||||
expect(findResult2.docs[1].title === 'Title').toBeTruthy();
|
||||
});
|
||||
|
||||
it('should pass context from beforeChange to afterChange', async () => {
|
||||
const document = await payload.create({
|
||||
collection: contextHooksSlug,
|
||||
|
||||
@@ -5,11 +5,24 @@
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "transforms".
|
||||
*/
|
||||
export interface Config {
|
||||
collections: {
|
||||
afterOperation: AfterOperation;
|
||||
transforms: Transform;
|
||||
hooks: Hook;
|
||||
'nested-after-read-hooks': NestedAfterReadHook;
|
||||
'chaining-hooks': ChainingHook;
|
||||
relations: Relation;
|
||||
'hooks-users': HooksUser;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface AfterOperation {
|
||||
id: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Transform {
|
||||
id: string;
|
||||
/**
|
||||
@@ -22,13 +35,9 @@ export interface Transform {
|
||||
* @maxItems 2
|
||||
*/
|
||||
localizedTransform?: [number, number];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "hooks".
|
||||
*/
|
||||
export interface Hook {
|
||||
id: string;
|
||||
fieldBeforeValidate?: boolean;
|
||||
@@ -40,53 +49,48 @@ export interface Hook {
|
||||
collectionAfterChange?: boolean;
|
||||
collectionBeforeRead?: boolean;
|
||||
collectionAfterRead?: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "nested-after-read-hooks".
|
||||
*/
|
||||
export interface NestedAfterReadHook {
|
||||
id: string;
|
||||
text?: string;
|
||||
group: {
|
||||
array: {
|
||||
group?: {
|
||||
array?: {
|
||||
input?: string;
|
||||
afterRead?: string;
|
||||
shouldPopulate?: string | Relation;
|
||||
id?: string;
|
||||
}[];
|
||||
subGroup: {
|
||||
subGroup?: {
|
||||
afterRead?: string;
|
||||
shouldPopulate?: string | Relation;
|
||||
};
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relations".
|
||||
*/
|
||||
export interface Relation {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface ChainingHook {
|
||||
id: string;
|
||||
text?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "hooks-users".
|
||||
*/
|
||||
export interface HooksUser {
|
||||
id: string;
|
||||
roles: ('admin' | 'user')[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email?: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('refresh-permissions', () => {
|
||||
await expect(page.locator('#nav-global-test')).toBeHidden();
|
||||
|
||||
// Allow access to test global.
|
||||
await page.locator('.custom-checkbox:has(#field-test) button').click();
|
||||
await page.locator('.custom-checkbox:has(#field-test) input').check();
|
||||
await page.locator('#action-save').click();
|
||||
|
||||
// Now test collection should appear in the menu.
|
||||
|
||||
@@ -190,6 +190,12 @@ export default buildConfigWithDefaults({
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'movies',
|
||||
type: 'relationship',
|
||||
relationTo: 'movies',
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { randomBytes } from 'crypto';
|
||||
import mongoose from 'mongoose';
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import config, { customIdSlug, chainedRelSlug, defaultAccessRelSlug, slug, relationSlug, customIdNumberSlug } from './config';
|
||||
import payload from '../../src';
|
||||
@@ -345,6 +345,64 @@ describe('Relationships', () => {
|
||||
expect(query.docs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
describe('Multiple Docs', () => {
|
||||
const movieList = [
|
||||
'Pulp Fiction',
|
||||
'Reservoir Dogs',
|
||||
'Once Upon a Time in Hollywood',
|
||||
'Shrek',
|
||||
'Shrek 2',
|
||||
'Shrek 3',
|
||||
'Scream',
|
||||
'The Matrix',
|
||||
'The Matrix Reloaded',
|
||||
'The Matrix Revolutions',
|
||||
'The Matrix Resurrections',
|
||||
'The Haunting',
|
||||
'The Haunting of Hill House',
|
||||
'The Haunting of Bly Manor',
|
||||
'Insidious',
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
await Promise.all(movieList.map((movie) => {
|
||||
return payload.create({
|
||||
collection: 'movies',
|
||||
data: {
|
||||
name: movie,
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should return more than 10 docs in relationship', async () => {
|
||||
const allMovies = await payload.find({
|
||||
collection: 'movies',
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const movieIDs = allMovies.docs.map((doc) => doc.id);
|
||||
|
||||
await payload.create({
|
||||
collection: 'directors',
|
||||
data: {
|
||||
name: 'Quentin Tarantino',
|
||||
movies: movieIDs,
|
||||
},
|
||||
});
|
||||
|
||||
const director = await payload.find({
|
||||
collection: 'directors',
|
||||
where: {
|
||||
name: {
|
||||
equals: 'Quentin Tarantino',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(director.docs[0].movies.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user