feat: lock documents while being edited (#7970)

## Description

Adds a new property to `collection` / `global` configs called
`lockDocuments`.

Set to `true` by default - the lock is automatically triggered when a
user begins editing a document within the Admin Panel and remains in
place until the user exits the editing view or the lock expires due to
inactivity.

Set to `false` to disable document locking entirely - i.e.
`lockDocuments: false`

You can pass an object to this property to configure the `duration` in
seconds, which defines how long the document remains locked without user
interaction. If no edits are made within the specified time (default:
300 seconds), the lock expires, allowing other users to edit / update or
delete the document.

```
lockDocuments: {
  duration: 180, // 180 seconds or 3 minutes
}
```

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

## Type of change

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

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
This commit is contained in:
Patrik
2024-09-17 14:04:48 -04:00
committed by GitHub
parent 05a3cc47a6
commit f98d032617
119 changed files with 10897 additions and 6575 deletions

View File

@@ -310,6 +310,7 @@ jobs:
- fields__collections__Upload
- live-preview
- localization
- locked-documents
- i18n
- plugin-cloud-storage
- plugin-form-builder

7
.vscode/launch.json vendored
View File

@@ -118,6 +118,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js locked-documents",
"cwd": "${workspaceFolder}",
"name": "Run Dev Locked Documents",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js uploads",
"cwd": "${workspaceFolder}",

View File

@@ -0,0 +1,60 @@
---
title: Document Locking
label: Document Locking
order: 90
desc: Ensure your documents are locked while being edited, preventing concurrent edits from multiple users and preserving data integrity.
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
---
Document locking in Payload ensures that only one user at a time can edit a document, preventing data conflicts and accidental overwrites. When a document is locked, other users are prevented from making changes until the lock is released, ensuring data integrity in collaborative environments.
The lock is automatically triggered when a user begins editing a document within the Admin Panel and remains in place until the user exits the editing view or the lock expires due to inactivity.
## How it works
When a user starts editing a document, Payload locks the document for that user. If another user tries to access the same document, they will be notified that it is currently being edited and can choose one of the following options:
- View in Read-Only Mode: View the document without making any changes.
- Take Over Editing: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
- Return to Dashboard: Navigate away from the locked document and continue with other tasks.
The lock will automatically expire after a set period of inactivity, configurable using the duration property in the lockDocuments configuration, after which others can resume editing.
<Banner type="info"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
### Config Options
The lockDocuments property exists on both the Collection Config and the Global Config. By default, document locking is enabled for all collections and globals, but you can customize the lock duration or disable the feature entirely.
Heres an example configuration for document locking:
```ts
import { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
},
// other fields...
],
lockDocuments: {
duration: 600, // Duration in seconds
},
}
```
#### Locking Options
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`lockDocuments`** | Enables or disables document locking for the collection or global. By default, document locking is enabled. Set to an object to configure, or set to false to disable locking. |
| **`duration`** | Specifies the duration (in seconds) for how long a document remains locked without user interaction. The default is 300 seconds (5 minutes). |
### Impact on APIs
Document locking affects both the Local API and the REST API, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.

View File

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

View File

@@ -65,21 +65,22 @@ export const Nav: GlobalConfig = {
The following options are available:
| Option | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/globals). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`description`** | Text or React component to display below the Global header to give editors more information. |
| **`endpoints`** | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
| **`label`** | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#globals-config). |
| Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/globals). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`description`** | Text or React component to display below the Global header to give editors more information. |
| **`endpoints`** | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
| **`label`** | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#globals-config). |
_\* An asterisk denotes that a property is required._

View File

@@ -55,59 +55,108 @@ import { CustomDefaultRootView as CustomDefaultRootView_53 } from '@/components/
import { CustomMinimalRootView as CustomMinimalRootView_54 } from '@/components/views/CustomMinimalRootView'
export const importMap = {
"@/collections/Fields/array/components/server/Label#CustomArrayFieldLabelServer": CustomArrayFieldLabelServer_0,
"@/collections/Fields/array/components/server/Field#CustomArrayFieldServer": CustomArrayFieldServer_1,
"@/collections/Fields/array/components/client/Label#CustomArrayFieldLabelClient": CustomArrayFieldLabelClient_2,
"@/collections/Fields/array/components/client/Field#CustomArrayFieldClient": CustomArrayFieldClient_3,
"@/collections/Fields/blocks/components/server/Field#CustomBlocksFieldServer": CustomBlocksFieldServer_4,
"@/collections/Fields/blocks/components/client/Field#CustomBlocksFieldClient": CustomBlocksFieldClient_5,
"@/collections/Fields/checkbox/components/server/Label#CustomCheckboxFieldLabelServer": CustomCheckboxFieldLabelServer_6,
"@/collections/Fields/checkbox/components/server/Field#CustomCheckboxFieldServer": CustomCheckboxFieldServer_7,
"@/collections/Fields/checkbox/components/client/Label#CustomCheckboxFieldLabelClient": CustomCheckboxFieldLabelClient_8,
"@/collections/Fields/checkbox/components/client/Field#CustomCheckboxFieldClient": CustomCheckboxFieldClient_9,
"@/collections/Fields/date/components/server/Label#CustomDateFieldLabelServer": CustomDateFieldLabelServer_10,
"@/collections/Fields/date/components/server/Field#CustomDateFieldServer": CustomDateFieldServer_11,
"@/collections/Fields/date/components/client/Label#CustomDateFieldLabelClient": CustomDateFieldLabelClient_12,
"@/collections/Fields/date/components/client/Field#CustomDateFieldClient": CustomDateFieldClient_13,
"@/collections/Fields/email/components/server/Label#CustomEmailFieldLabelServer": CustomEmailFieldLabelServer_14,
"@/collections/Fields/email/components/server/Field#CustomEmailFieldServer": CustomEmailFieldServer_15,
"@/collections/Fields/email/components/client/Label#CustomEmailFieldLabelClient": CustomEmailFieldLabelClient_16,
"@/collections/Fields/email/components/client/Field#CustomEmailFieldClient": CustomEmailFieldClient_17,
"@/collections/Fields/number/components/server/Label#CustomNumberFieldLabelServer": CustomNumberFieldLabelServer_18,
"@/collections/Fields/number/components/server/Field#CustomNumberFieldServer": CustomNumberFieldServer_19,
"@/collections/Fields/number/components/client/Label#CustomNumberFieldLabelClient": CustomNumberFieldLabelClient_20,
"@/collections/Fields/number/components/client/Field#CustomNumberFieldClient": CustomNumberFieldClient_21,
"@/collections/Fields/point/components/server/Label#CustomPointFieldLabelServer": CustomPointFieldLabelServer_22,
"@/collections/Fields/point/components/server/Field#CustomPointFieldServer": CustomPointFieldServer_23,
"@/collections/Fields/point/components/client/Label#CustomPointFieldLabelClient": CustomPointFieldLabelClient_24,
"@/collections/Fields/point/components/client/Field#CustomPointFieldClient": CustomPointFieldClient_25,
"@/collections/Fields/radio/components/server/Label#CustomRadioFieldLabelServer": CustomRadioFieldLabelServer_26,
"@/collections/Fields/radio/components/server/Field#CustomRadioFieldServer": CustomRadioFieldServer_27,
"@/collections/Fields/radio/components/client/Label#CustomRadioFieldLabelClient": CustomRadioFieldLabelClient_28,
"@/collections/Fields/radio/components/client/Field#CustomRadioFieldClient": CustomRadioFieldClient_29,
"@/collections/Fields/relationship/components/server/Label#CustomRelationshipFieldLabelServer": CustomRelationshipFieldLabelServer_30,
"@/collections/Fields/relationship/components/server/Field#CustomRelationshipFieldServer": CustomRelationshipFieldServer_31,
"@/collections/Fields/relationship/components/client/Label#CustomRelationshipFieldLabelClient": CustomRelationshipFieldLabelClient_32,
"@/collections/Fields/relationship/components/client/Field#CustomRelationshipFieldClient": CustomRelationshipFieldClient_33,
"@/collections/Fields/select/components/server/Label#CustomSelectFieldLabelServer": CustomSelectFieldLabelServer_34,
"@/collections/Fields/select/components/server/Field#CustomSelectFieldServer": CustomSelectFieldServer_35,
"@/collections/Fields/select/components/client/Label#CustomSelectFieldLabelClient": CustomSelectFieldLabelClient_36,
"@/collections/Fields/select/components/client/Field#CustomSelectFieldClient": CustomSelectFieldClient_37,
"@/collections/Fields/text/components/server/Label#CustomTextFieldLabelServer": CustomTextFieldLabelServer_38,
"@/collections/Fields/text/components/server/Field#CustomTextFieldServer": CustomTextFieldServer_39,
"@/collections/Fields/text/components/client/Label#CustomTextFieldLabelClient": CustomTextFieldLabelClient_40,
"@/collections/Fields/text/components/client/Field#CustomTextFieldClient": CustomTextFieldClient_41,
"@/collections/Fields/textarea/components/server/Label#CustomTextareaFieldLabelServer": CustomTextareaFieldLabelServer_42,
"@/collections/Fields/textarea/components/server/Field#CustomTextareaFieldServer": CustomTextareaFieldServer_43,
"@/collections/Fields/textarea/components/client/Label#CustomTextareaFieldLabelClient": CustomTextareaFieldLabelClient_44,
"@/collections/Fields/textarea/components/client/Field#CustomTextareaFieldClient": CustomTextareaFieldClient_45,
"@/collections/Views/components/CustomTabEditView#CustomTabEditView": CustomTabEditView_46,
"@/collections/Views/components/CustomDefaultEditView#CustomDefaultEditView": CustomDefaultEditView_47,
"@/collections/RootViews/components/CustomRootEditView#CustomRootEditView": CustomRootEditView_48,
"@/components/afterNavLinks/LinkToCustomView#LinkToCustomView": LinkToCustomView_49,
"@/components/afterNavLinks/LinkToCustomMinimalView#LinkToCustomMinimalView": LinkToCustomMinimalView_50,
"@/components/afterNavLinks/LinkToCustomDefaultView#LinkToCustomDefaultView": LinkToCustomDefaultView_51,
"@/components/views/CustomRootView#CustomRootView": CustomRootView_52,
"@/components/views/CustomDefaultRootView#CustomDefaultRootView": CustomDefaultRootView_53,
"@/components/views/CustomMinimalRootView#CustomMinimalRootView": CustomMinimalRootView_54
'@/collections/Fields/array/components/server/Label#CustomArrayFieldLabelServer':
CustomArrayFieldLabelServer_0,
'@/collections/Fields/array/components/server/Field#CustomArrayFieldServer':
CustomArrayFieldServer_1,
'@/collections/Fields/array/components/client/Label#CustomArrayFieldLabelClient':
CustomArrayFieldLabelClient_2,
'@/collections/Fields/array/components/client/Field#CustomArrayFieldClient':
CustomArrayFieldClient_3,
'@/collections/Fields/blocks/components/server/Field#CustomBlocksFieldServer':
CustomBlocksFieldServer_4,
'@/collections/Fields/blocks/components/client/Field#CustomBlocksFieldClient':
CustomBlocksFieldClient_5,
'@/collections/Fields/checkbox/components/server/Label#CustomCheckboxFieldLabelServer':
CustomCheckboxFieldLabelServer_6,
'@/collections/Fields/checkbox/components/server/Field#CustomCheckboxFieldServer':
CustomCheckboxFieldServer_7,
'@/collections/Fields/checkbox/components/client/Label#CustomCheckboxFieldLabelClient':
CustomCheckboxFieldLabelClient_8,
'@/collections/Fields/checkbox/components/client/Field#CustomCheckboxFieldClient':
CustomCheckboxFieldClient_9,
'@/collections/Fields/date/components/server/Label#CustomDateFieldLabelServer':
CustomDateFieldLabelServer_10,
'@/collections/Fields/date/components/server/Field#CustomDateFieldServer':
CustomDateFieldServer_11,
'@/collections/Fields/date/components/client/Label#CustomDateFieldLabelClient':
CustomDateFieldLabelClient_12,
'@/collections/Fields/date/components/client/Field#CustomDateFieldClient':
CustomDateFieldClient_13,
'@/collections/Fields/email/components/server/Label#CustomEmailFieldLabelServer':
CustomEmailFieldLabelServer_14,
'@/collections/Fields/email/components/server/Field#CustomEmailFieldServer':
CustomEmailFieldServer_15,
'@/collections/Fields/email/components/client/Label#CustomEmailFieldLabelClient':
CustomEmailFieldLabelClient_16,
'@/collections/Fields/email/components/client/Field#CustomEmailFieldClient':
CustomEmailFieldClient_17,
'@/collections/Fields/number/components/server/Label#CustomNumberFieldLabelServer':
CustomNumberFieldLabelServer_18,
'@/collections/Fields/number/components/server/Field#CustomNumberFieldServer':
CustomNumberFieldServer_19,
'@/collections/Fields/number/components/client/Label#CustomNumberFieldLabelClient':
CustomNumberFieldLabelClient_20,
'@/collections/Fields/number/components/client/Field#CustomNumberFieldClient':
CustomNumberFieldClient_21,
'@/collections/Fields/point/components/server/Label#CustomPointFieldLabelServer':
CustomPointFieldLabelServer_22,
'@/collections/Fields/point/components/server/Field#CustomPointFieldServer':
CustomPointFieldServer_23,
'@/collections/Fields/point/components/client/Label#CustomPointFieldLabelClient':
CustomPointFieldLabelClient_24,
'@/collections/Fields/point/components/client/Field#CustomPointFieldClient':
CustomPointFieldClient_25,
'@/collections/Fields/radio/components/server/Label#CustomRadioFieldLabelServer':
CustomRadioFieldLabelServer_26,
'@/collections/Fields/radio/components/server/Field#CustomRadioFieldServer':
CustomRadioFieldServer_27,
'@/collections/Fields/radio/components/client/Label#CustomRadioFieldLabelClient':
CustomRadioFieldLabelClient_28,
'@/collections/Fields/radio/components/client/Field#CustomRadioFieldClient':
CustomRadioFieldClient_29,
'@/collections/Fields/relationship/components/server/Label#CustomRelationshipFieldLabelServer':
CustomRelationshipFieldLabelServer_30,
'@/collections/Fields/relationship/components/server/Field#CustomRelationshipFieldServer':
CustomRelationshipFieldServer_31,
'@/collections/Fields/relationship/components/client/Label#CustomRelationshipFieldLabelClient':
CustomRelationshipFieldLabelClient_32,
'@/collections/Fields/relationship/components/client/Field#CustomRelationshipFieldClient':
CustomRelationshipFieldClient_33,
'@/collections/Fields/select/components/server/Label#CustomSelectFieldLabelServer':
CustomSelectFieldLabelServer_34,
'@/collections/Fields/select/components/server/Field#CustomSelectFieldServer':
CustomSelectFieldServer_35,
'@/collections/Fields/select/components/client/Label#CustomSelectFieldLabelClient':
CustomSelectFieldLabelClient_36,
'@/collections/Fields/select/components/client/Field#CustomSelectFieldClient':
CustomSelectFieldClient_37,
'@/collections/Fields/text/components/server/Label#CustomTextFieldLabelServer':
CustomTextFieldLabelServer_38,
'@/collections/Fields/text/components/server/Field#CustomTextFieldServer':
CustomTextFieldServer_39,
'@/collections/Fields/text/components/client/Label#CustomTextFieldLabelClient':
CustomTextFieldLabelClient_40,
'@/collections/Fields/text/components/client/Field#CustomTextFieldClient':
CustomTextFieldClient_41,
'@/collections/Fields/textarea/components/server/Label#CustomTextareaFieldLabelServer':
CustomTextareaFieldLabelServer_42,
'@/collections/Fields/textarea/components/server/Field#CustomTextareaFieldServer':
CustomTextareaFieldServer_43,
'@/collections/Fields/textarea/components/client/Label#CustomTextareaFieldLabelClient':
CustomTextareaFieldLabelClient_44,
'@/collections/Fields/textarea/components/client/Field#CustomTextareaFieldClient':
CustomTextareaFieldClient_45,
'@/collections/Views/components/CustomTabEditView#CustomTabEditView': CustomTabEditView_46,
'@/collections/Views/components/CustomDefaultEditView#CustomDefaultEditView':
CustomDefaultEditView_47,
'@/collections/RootViews/components/CustomRootEditView#CustomRootEditView': CustomRootEditView_48,
'@/components/afterNavLinks/LinkToCustomView#LinkToCustomView': LinkToCustomView_49,
'@/components/afterNavLinks/LinkToCustomMinimalView#LinkToCustomMinimalView':
LinkToCustomMinimalView_50,
'@/components/afterNavLinks/LinkToCustomDefaultView#LinkToCustomDefaultView':
LinkToCustomDefaultView_51,
'@/components/views/CustomRootView#CustomRootView': CustomRootView_52,
'@/components/views/CustomDefaultRootView#CustomDefaultRootView': CustomDefaultRootView_53,
'@/components/views/CustomMinimalRootView#CustomMinimalRootView': CustomMinimalRootView_54,
}

View File

@@ -261,21 +261,34 @@ export function buildObjectType({
let type
let relationToType = null
const graphQLCollections = config.collections.filter(
(collectionConfig) => collectionConfig.graphQL !== false,
)
if (Array.isArray(relationTo)) {
relationToType = new GraphQLEnumType({
name: `${relationshipName}_RelationTo`,
values: relationTo.reduce(
(relations, relation) => ({
...relations,
[formatName(relation)]: {
value: relation,
},
}),
{},
),
values: relationTo
.filter((relation) =>
graphQLCollections.some((collection) => collection.slug === relation),
)
.reduce(
(relations, relation) => ({
...relations,
[formatName(relation)]: {
value: relation,
},
}),
{},
),
})
const types = relationTo.map((relation) => graphqlResult.collections[relation].graphQL.type)
// Only pass collections that are GraphQL enabled
const types = relationTo
.filter((relation) =>
graphQLCollections.some((collection) => collection.slug === relation),
)
.map((relation) => graphqlResult.collections[relation]?.graphQL.type)
type = new GraphQLObjectType({
name: `${relationshipName}_Relationship`,
@@ -314,9 +327,9 @@ export function buildObjectType({
where?: unknown
} = {}
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo]).some(
(relation) => graphqlResult.collections[relation].config.versions?.drafts,
)
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo])
.filter((relation) => graphQLCollections.some((collection) => collection.slug === relation))
.some((relation) => graphqlResult.collections[relation].config.versions?.drafts)
if (relationsUseDrafts) {
relationshipArgs.draft = {
@@ -357,37 +370,39 @@ export function buildObjectType({
let id = relatedDoc
let collectionSlug = field.relationTo
if (isRelatedToManyCollections) {
collectionSlug = relatedDoc.relationTo
id = relatedDoc.value
}
const result = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: collectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)
if (result) {
if (graphQLCollections.some((collection) => collection.slug === collectionSlug)) {
if (isRelatedToManyCollections) {
results[i] = {
relationTo: collectionSlug,
value: {
...result,
collection: collectionSlug,
},
collectionSlug = relatedDoc.relationTo
id = relatedDoc.value
}
const result = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: collectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)
if (result) {
if (isRelatedToManyCollections) {
results[i] = {
relationTo: collectionSlug,
value: {
...result,
collection: collectionSlug,
},
}
} else {
results[i] = result
}
} else {
results[i] = result
}
}
}
@@ -409,33 +424,37 @@ export function buildObjectType({
}
if (id) {
const relatedDocument = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: relatedCollectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)
if (
graphQLCollections.some((collection) => collection.slug === relatedCollectionSlug)
) {
const relatedDocument = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: relatedCollectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)
if (relatedDocument) {
if (isRelatedToManyCollections) {
return {
relationTo: relatedCollectionSlug,
value: {
...relatedDocument,
collection: relatedCollectionSlug,
},
if (relatedDocument) {
if (isRelatedToManyCollections) {
return {
relationTo: relatedCollectionSlug,
value: {
...relatedDocument,
collection: relatedCollectionSlug,
},
}
}
}
return relatedDocument
return relatedDocument
}
}
return null

View File

@@ -0,0 +1,37 @@
@import '../../scss/styles.scss';
.document-locked {
@include blur-bg;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&__wrapper {
z-index: 1;
position: relative;
display: flex;
flex-direction: column;
gap: var(--base);
padding: base(2);
}
&__content {
display: flex;
flex-direction: column;
gap: var(--base);
> * {
margin: 0;
}
}
&__controls {
display: flex;
gap: var(--base);
.btn {
margin: 0;
}
}
}

View File

@@ -0,0 +1,93 @@
'use client'
import type { ClientUser } from 'payload'
import { Button, Modal, useModal, useTranslation } from '@payloadcms/ui'
import React, { useEffect } from 'react'
import './index.scss'
const modalSlug = 'document-locked'
const baseClass = 'document-locked'
const formatDate = (date) => {
if (!date) {
return ''
}
return new Intl.DateTimeFormat('en-US', {
day: 'numeric',
hour: 'numeric',
hour12: true,
minute: 'numeric',
month: 'short',
year: 'numeric',
}).format(date)
}
export const DocumentLocked: React.FC<{
editedAt?: null | number
handleGoBack: () => void
isActive: boolean
onReadOnly: () => void
onTakeOver: () => void
user?: ClientUser
}> = ({ editedAt, handleGoBack, isActive, onReadOnly, onTakeOver, user }) => {
const { closeModal, openModal } = useModal()
const { t } = useTranslation()
useEffect(() => {
if (isActive) {
openModal(modalSlug)
} else {
closeModal(modalSlug)
}
}, [isActive, openModal, closeModal])
return (
<Modal className={baseClass} onClose={handleGoBack} slug={modalSlug}>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__content`}>
<h1>{t('general:documentLocked')}</h1>
<p>
<strong>{user?.email ?? user?.id}</strong> {t('general:currentlyEditing')}
</p>
<p>
{t('general:editedSince')} <strong>{formatDate(editedAt)}</strong>
</p>
</div>
<div className={`${baseClass}__controls`}>
<Button
buttonStyle="secondary"
id={`${modalSlug}-go-back`}
onClick={handleGoBack}
size="large"
>
{t('general:goBack')}
</Button>
<Button
buttonStyle="secondary"
id={`${modalSlug}-view-read-only`}
onClick={() => {
onReadOnly()
closeModal(modalSlug)
}}
size="large"
>
{t('general:viewReadOnly')}
</Button>
<Button
buttonStyle="primary"
id={`${modalSlug}-take-over`}
onClick={() => {
void onTakeOver()
closeModal(modalSlug)
}}
size="large"
>
{t('general:takeOver')}
</Button>
</div>
</div>
</Modal>
)
}

View File

@@ -0,0 +1,37 @@
@import '../../scss/styles.scss';
.document-take-over {
@include blur-bg;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&__wrapper {
z-index: 1;
position: relative;
display: flex;
flex-direction: column;
gap: var(--base);
padding: base(2);
}
&__content {
display: flex;
flex-direction: column;
gap: var(--base);
> * {
margin: 0;
}
}
&__controls {
display: flex;
gap: var(--base);
.btn {
margin: 0;
}
}
}

View File

@@ -0,0 +1,58 @@
'use client'
import { Button, Modal, useModal, useTranslation } from '@payloadcms/ui'
import React, { useEffect } from 'react'
import './index.scss'
const modalSlug = 'document-take-over'
const baseClass = 'document-take-over'
export const DocumentTakeOver: React.FC<{
handleBackToDashboard: () => void
isActive: boolean
onReadOnly: () => void
}> = ({ handleBackToDashboard, isActive, onReadOnly }) => {
const { closeModal, openModal } = useModal()
const { t } = useTranslation()
useEffect(() => {
if (isActive) {
openModal(modalSlug)
} else {
closeModal(modalSlug)
}
}, [isActive, openModal, closeModal])
return (
<Modal className={baseClass} slug={modalSlug}>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__content`}>
<h1>{t('general:editingTakenOver')}</h1>
<p>{t('general:anotherUserTakenOver')}</p>
</div>
<div className={`${baseClass}__controls`}>
<Button
buttonStyle="primary"
id={`${modalSlug}-back-to-dashboard`}
onClick={handleBackToDashboard}
size="large"
>
{t('general:backToDashboard')}
</Button>
<Button
buttonStyle="secondary"
id={`${modalSlug}-view-read-only`}
onClick={() => {
onReadOnly()
closeModal(modalSlug)
}}
size="large"
>
{t('general:viewReadOnly')}
</Button>
</div>
</div>
</Modal>
)
}

View File

@@ -34,8 +34,8 @@ export const CreateFirstUserClient: React.FC<{
const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig
const onChange: FormProps['onChange'][0] = React.useCallback(
async ({ formState: prevFormState }) =>
getFormState({
async ({ formState: prevFormState }) => {
const { state } = await getFormState({
apiRoute,
body: {
collectionSlug: userSlug,
@@ -44,7 +44,9 @@ export const CreateFirstUserClient: React.FC<{
schemaPath: `_${userSlug}.auth`,
},
serverURL,
}),
})
return state
},
[apiRoute, userSlug, serverURL],
)

View File

@@ -35,6 +35,11 @@
}
}
&__locked.locked {
align-items: unset;
justify-content: unset;
}
@include large-break {
--cols: 4;
}

View File

@@ -1,8 +1,8 @@
import type { groupNavItems } from '@payloadcms/ui/shared'
import type { Permissions, ServerProps, VisibleEntities } from 'payload'
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { Button, Card, Gutter, SetStepNav, SetViewActions } from '@payloadcms/ui'
import { Button, Card, Gutter, Locked, SetStepNav, SetViewActions } from '@payloadcms/ui'
import {
EntityType,
formatAdminURL,
@@ -16,6 +16,7 @@ import './index.scss'
const baseClass = 'dashboard'
export type DashboardProps = {
globalData: Array<{ data: { isLocked: boolean; userEditing: ClientUser | null }; slug: string }>
Link: React.ComponentType<any>
navGroups?: ReturnType<typeof groupNavItems>
permissions: Permissions
@@ -24,6 +25,7 @@ export type DashboardProps = {
export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
const {
globalData,
i18n,
i18n: { t },
Link,
@@ -93,6 +95,8 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
let createHREF: string
let href: string
let hasCreatePermission: boolean
let lockStatus = null
let userEditing = null
if (type === EntityType.collection) {
title = getTranslation(entity.labels.plural, i18n)
@@ -121,13 +125,24 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
adminRoute,
path: `/globals/${entity.slug}`,
})
// Find the lock status for the global
const globalLockData = globalData.find(
(global) => global.slug === entity.slug,
)
if (globalLockData) {
lockStatus = globalLockData.data.isLocked
userEditing = globalLockData.data.userEditing
}
}
return (
<li key={entityIndex}>
<Card
actions={
hasCreatePermission && type === EntityType.collection ? (
lockStatus ? (
<Locked className={`${baseClass}__locked`} user={userEditing} />
) : hasCreatePermission && type === EntityType.collection ? (
<Button
aria-label={t('general:createNewLabel', {
label: getTranslation(entity.labels.singular, i18n),

View File

@@ -17,7 +17,11 @@ export { generateDashboardMetadata } from './meta.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult, params, searchParams }) => {
export const Dashboard: React.FC<AdminViewProps> = async ({
initPageResult,
params,
searchParams,
}) => {
const {
locale,
permissions,
@@ -44,6 +48,29 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult, params, se
visibleEntities.globals.includes(global.slug),
)
const globalSlugs = config.globals.map((global) => global.slug)
// Filter the slugs based on permissions and visibility
const filteredGlobalSlugs = globalSlugs.filter(
(slug) =>
permissions?.globals?.[slug]?.read?.permission && visibleEntities.globals.includes(slug),
)
const globalData = await Promise.all(
filteredGlobalSlugs.map(async (slug) => {
const data = await payload.findGlobal({
slug,
depth: 0,
includeLockStatus: true,
})
return {
slug,
data,
}
}),
)
const navGroups = groupNavItems(
[
...(collections.map((collection) => {
@@ -70,6 +97,7 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult, params, se
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
globalData,
i18n,
Link,
locale,

View File

@@ -1,5 +1,6 @@
import type {
Data,
FormState,
Locale,
PayloadRequest,
SanitizedCollectionConfig,
@@ -16,13 +17,16 @@ export const getDocumentData = async (args: {
locale: Locale
req: PayloadRequest
schemaPath?: string
}): Promise<Data> => {
}): Promise<{
data: Data
formState: FormState
}> => {
const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args
const schemaPath = schemaPathFromProps || collectionConfig?.slug || globalConfig?.slug
try {
const formState = await buildFormState({
const { state: formState } = await buildFormState({
req: {
...req,
data: {
@@ -44,6 +48,15 @@ export const getDocumentData = async (args: {
}
} catch (error) {
console.error('Error getting document data', error) // eslint-disable-line no-console
return {}
return {
data: {},
formState: {
fields: {
initialValue: undefined,
valid: false,
value: undefined,
},
},
}
}
}

View File

@@ -1,6 +1,6 @@
'use client'
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
import type { ClientCollectionConfig, ClientGlobalConfig, ClientUser } from 'payload'
import {
DocumentControls,
@@ -19,8 +19,10 @@ import {
} from '@payloadcms/ui'
import { formatAdminURL, getFormState } from '@payloadcms/ui/shared'
import { useRouter, useSearchParams } from 'next/navigation.js'
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { DocumentLocked } from '../../../elements/DocumentLocked/index.js'
import { DocumentTakeOver } from '../../../elements/DocumentTakeOver/index.js'
import { LeaveWithoutSaving } from '../../../elements/LeaveWithoutSaving/index.js'
import { Auth } from './Auth/index.js'
import './index.scss'
@@ -42,10 +44,12 @@ export const DefaultEditView: React.FC = () => {
BeforeDocument,
BeforeFields,
collectionSlug,
currentEditor,
disableActions,
disableCreate,
disableLeaveWithoutSaving,
docPermissions,
documentIsLocked,
getDocPreferences,
getVersions,
globalSlug,
@@ -61,10 +65,13 @@ export const DefaultEditView: React.FC = () => {
onSave: onSaveFromContext,
redirectAfterDelete,
redirectAfterDuplicate,
setCurrentEditor,
setDocumentIsLocked,
unlockDocument,
updateDocumentEditor,
} = useDocumentInfo()
const { refreshCookieAsync, user } = useAuth()
const {
config,
config: {
@@ -94,12 +101,33 @@ export const DefaultEditView: React.FC = () => {
const auth = collectionConfig ? collectionConfig.auth : undefined
const upload = collectionConfig ? collectionConfig.upload : undefined
const docConfig = collectionConfig || globalConfig
const lockDocumentsProp = docConfig?.lockDocuments !== undefined ? docConfig?.lockDocuments : true
const isLockingEnabled = lockDocumentsProp !== false
const preventLeaveWithoutSaving =
(!(collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
!(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave)) &&
!disableLeaveWithoutSaving
const classes = [baseClass, id && `${baseClass}--is-editing`]
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
const documentLockStateRef = useRef<{
hasShownLockedModal: boolean
isLocked: boolean
user: ClientUser
} | null>({
hasShownLockedModal: false,
isLocked: false,
user: null,
})
const [lastUpdateTime, setLastUpdateTime] = useState(Date.now())
const classes = [baseClass, (id || globalSlug) && `${baseClass}--is-editing`]
if (globalSlug) {
classes.push(`global-edit--${globalSlug}`)
@@ -108,7 +136,7 @@ export const DefaultEditView: React.FC = () => {
classes.push(`collection-edit--${collectionSlug}`)
}
const [schemaPath, setSchemaPath] = React.useState(() => {
const [schemaPath, setSchemaPath] = useState(() => {
if (operation === 'create' && auth && !auth.disableLocalStrategy) {
return `_${entitySlug}.auth`
}
@@ -123,6 +151,89 @@ export const DefaultEditView: React.FC = () => {
return false
})
const handleTakeOver = useCallback(() => {
if (!isLockingEnabled) {
return
}
try {
// Call updateDocumentEditor to update the document's owner to the current user
void updateDocumentEditor(id, collectionSlug ?? globalSlug, user)
documentLockStateRef.current.hasShownLockedModal = true
// Update the locked state to reflect the current user as the owner
documentLockStateRef.current = {
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal,
isLocked: true,
user,
}
setCurrentEditor(user)
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error during document takeover:', error)
}
}, [
updateDocumentEditor,
id,
collectionSlug,
globalSlug,
user,
setCurrentEditor,
isLockingEnabled,
])
const handleTakeOverWithinDoc = useCallback(() => {
if (!isLockingEnabled) {
return
}
try {
// Call updateDocumentEditor to update the document's owner to the current user
void updateDocumentEditor(id, collectionSlug ?? globalSlug, user)
// Update the locked state to reflect the current user as the owner
documentLockStateRef.current = {
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal,
isLocked: true,
user,
}
setCurrentEditor(user)
// Ensure the document is editable for the incoming user
setIsReadOnlyForIncomingUser(false)
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error during document takeover:', error)
}
}, [
updateDocumentEditor,
id,
collectionSlug,
globalSlug,
user,
setCurrentEditor,
isLockingEnabled,
])
const handleGoBack = useCallback(() => {
const redirectRoute = formatAdminURL({
adminRoute,
path: collectionSlug ? `/collections/${collectionSlug}` : '/',
})
router.push(redirectRoute)
}, [adminRoute, collectionSlug, router])
const handleBackToDashboard = useCallback(() => {
setShowTakeOverModal(false)
const redirectRoute = formatAdminURL({
adminRoute,
path: '/',
})
router.push(redirectRoute)
}, [adminRoute, router])
const onSave = useCallback(
(json) => {
reportUpdate({
@@ -146,6 +257,11 @@ export const DefaultEditView: React.FC = () => {
})
}
// Unlock the document after save
if ((id || globalSlug) && isLockingEnabled) {
setDocumentIsLocked(false)
}
if (!isEditing && depth < 2) {
// Redirect to the same locale if it's been set
const redirectRoute = formatAdminURL({
@@ -173,13 +289,26 @@ export const DefaultEditView: React.FC = () => {
router,
locale,
resetUploadEdits,
globalSlug,
isLockingEnabled,
setDocumentIsLocked,
],
)
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
const currentTime = Date.now()
const timeSinceLastUpdate = currentTime - lastUpdateTime
const updateLastEdited = isLockingEnabled && timeSinceLastUpdate >= 10000 // 10 seconds
if (updateLastEdited) {
setLastUpdateTime(currentTime)
}
const docPreferences = await getDocPreferences()
return getFormState({
const { lockedState, state } = await getFormState({
apiRoute,
body: {
id,
@@ -188,21 +317,100 @@ export const DefaultEditView: React.FC = () => {
formState: prevFormState,
globalSlug,
operation,
returnLockStatus: isLockingEnabled ? true : false,
schemaPath,
updateLastEdited,
},
serverURL,
})
setDocumentIsLocked(true)
if (isLockingEnabled) {
const previousOwnerId = documentLockStateRef.current?.user?.id
if (lockedState) {
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
setShowTakeOverModal(true)
documentLockStateRef.current.hasShownLockedModal = true
}
documentLockStateRef.current = documentLockStateRef.current = {
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
isLocked: true,
user: lockedState.user,
}
setCurrentEditor(lockedState.user)
}
}
}
return state
},
[apiRoute, collectionSlug, schemaPath, getDocPreferences, globalSlug, id, operation, serverURL],
[
apiRoute,
collectionSlug,
schemaPath,
getDocPreferences,
globalSlug,
id,
operation,
serverURL,
user,
documentLockStateRef,
setCurrentEditor,
isLockingEnabled,
setDocumentIsLocked,
lastUpdateTime,
],
)
// Clean up when the component unmounts or when the document is unlocked
useEffect(() => {
return () => {
if (!isLockingEnabled) {
return
}
if ((id || globalSlug) && documentIsLocked) {
// Check if this user is still the current editor
if (documentLockStateRef.current?.user?.id === user.id) {
void unlockDocument(id, collectionSlug ?? globalSlug)
setDocumentIsLocked(false)
setCurrentEditor(null)
}
}
setShowTakeOverModal(false)
}
}, [
collectionSlug,
globalSlug,
id,
unlockDocument,
user.id,
setCurrentEditor,
isLockingEnabled,
documentIsLocked,
setDocumentIsLocked,
])
const shouldShowDocumentLockedModal =
documentIsLocked &&
currentEditor &&
currentEditor.id !== user.id &&
!isReadOnlyForIncomingUser &&
!showTakeOverModal &&
!documentLockStateRef.current?.hasShownLockedModal
return (
<main className={classes.filter(Boolean).join(' ')}>
<OperationProvider operation={operation}>
<Form
action={action}
className={`${baseClass}__form`}
disabled={isInitializing || !hasSavePermission}
disabled={isReadOnlyForIncomingUser || isInitializing || !hasSavePermission}
disableValidationOnSubmit={!validateBeforeSubmit}
initialState={!isInitializing && initialState}
isInitializing={isInitializing}
@@ -211,7 +419,30 @@ export const DefaultEditView: React.FC = () => {
onSuccess={onSave}
>
{BeforeDocument}
{preventLeaveWithoutSaving && <LeaveWithoutSaving />}
{isLockingEnabled && shouldShowDocumentLockedModal && !isReadOnlyForIncomingUser && (
<DocumentLocked
editedAt={lastUpdateTime}
handleGoBack={handleGoBack}
isActive={shouldShowDocumentLockedModal}
onReadOnly={() => {
setIsReadOnlyForIncomingUser(true)
setShowTakeOverModal(false)
}}
onTakeOver={handleTakeOver}
user={currentEditor}
/>
)}
{isLockingEnabled && showTakeOverModal && (
<DocumentTakeOver
handleBackToDashboard={handleBackToDashboard}
isActive={showTakeOverModal}
onReadOnly={() => {
setIsReadOnlyForIncomingUser(true)
setShowTakeOverModal(false)
}}
/>
)}
{!isReadOnlyForIncomingUser && preventLeaveWithoutSaving && <LeaveWithoutSaving />}
<SetDocumentStepNav
collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug}
@@ -238,10 +469,13 @@ export const DefaultEditView: React.FC = () => {
onDrawerCreate={onDrawerCreate}
onDuplicate={onDuplicate}
onSave={onSave}
onTakeOver={handleTakeOverWithinDoc}
permissions={docPermissions}
readOnlyForIncomingUser={isReadOnlyForIncomingUser}
redirectAfterDelete={redirectAfterDelete}
redirectAfterDuplicate={redirectAfterDuplicate}
slug={collectionConfig?.slug || globalConfig?.slug}
user={currentEditor}
/>
<DocumentFields
AfterFields={AfterFields}
@@ -285,7 +519,7 @@ export const DefaultEditView: React.FC = () => {
}
docPermissions={docPermissions}
fields={(collectionConfig || globalConfig)?.fields}
readOnly={!hasSavePermission}
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
schemaPath={schemaPath}
/>
{AfterDocument}

View File

@@ -45,6 +45,7 @@
#heading-_select,
.cell-_select {
display: flex;
min-width: unset;
width: base(1);
}

View File

@@ -111,6 +111,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
depth: 0,
draft: true,
fallbackLocale: null,
includeLockStatus: true,
limit,
locale,
overrideAccess: false,

View File

@@ -117,7 +117,7 @@ const PreviewView: React.FC<Props> = ({
async ({ formState: prevFormState }) => {
const docPreferences = await getDocPreferences()
return getFormState({
const { state } = await getFormState({
apiRoute,
body: {
id,
@@ -128,6 +128,8 @@ const PreviewView: React.FC<Props> = ({
},
serverURL,
})
return state
},
[serverURL, apiRoute, id, operation, schemaPath, getDocPreferences],
)

View File

@@ -437,6 +437,15 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
plural?: LabelFunction | StaticLabel
singular?: LabelFunction | StaticLabel
}
/**
* Enables / Disables the ability to lock documents while editing
* @default true
*/
lockDocuments?:
| {
duration: number
}
| false
slug: string
/**
* Add `createdAt` and `updatedAt` fields
@@ -509,6 +518,7 @@ export type AuthCollection = {
}
export type TypeWithID = {
docId?: any
id: number | string
}

View File

@@ -12,6 +12,7 @@ import { APIError } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { deleteUserPreferences } from '../../preferences/deleteUserPreferences.js'
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles.js'
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
@@ -117,6 +118,17 @@ export const deleteOperation = async <TSlug extends CollectionSlug>(
const { id } = doc
try {
// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
const { lockedDocument, shouldUnlockDocument } = await checkDocumentLockStatus({
id,
collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`,
req,
})
// /////////////////////////////////////
// beforeDelete - Collection
// /////////////////////////////////////
@@ -140,6 +152,20 @@ export const deleteOperation = async <TSlug extends CollectionSlug>(
req,
})
// /////////////////////////////////////
// Unlock the document if necessary
// /////////////////////////////////////
if (shouldUnlockDocument && lockedDocument) {
await payload.db.deleteOne({
collection: 'payload-locked-documents',
req,
where: {
id: { equals: lockedDocument.id },
},
})
}
// /////////////////////////////////////
// Delete versions
// /////////////////////////////////////

View File

@@ -9,6 +9,7 @@ import { Forbidden, NotFound } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { deleteUserPreferences } from '../../preferences/deleteUserPreferences.js'
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles.js'
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
@@ -109,6 +110,17 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug>(
throw new Forbidden(req.t)
}
// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
const { lockedDocument, shouldUnlockDocument } = await checkDocumentLockStatus({
id,
collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`,
req,
})
await deleteAssociatedFiles({
collectionConfig,
config,
@@ -117,6 +129,20 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug>(
req,
})
// /////////////////////////////////////
// Unlock the document if necessary
// /////////////////////////////////////
if (shouldUnlockDocument && lockedDocument) {
await payload.db.deleteOne({
collection: 'payload-locked-documents',
req,
where: {
id: { equals: lockedDocument.id },
},
})
}
// /////////////////////////////////////
// Delete versions
// /////////////////////////////////////

View File

@@ -20,6 +20,7 @@ export type Arguments = {
depth?: number
disableErrors?: boolean
draft?: boolean
includeLockStatus?: boolean
limit?: number
overrideAccess?: boolean
page?: number
@@ -60,6 +61,7 @@ export const findOperation = async <TSlug extends CollectionSlug>(
depth,
disableErrors,
draft: draftsEnabled,
includeLockStatus,
limit,
overrideAccess,
page,
@@ -150,6 +152,49 @@ export const findOperation = async <TSlug extends CollectionSlug>(
})
}
if (includeLockStatus) {
try {
const lockedDocuments = await payload.find({
collection: 'payload-locked-documents',
depth: 1,
limit: sanitizedLimit,
pagination: false,
req,
where: {
and: [
{
'document.relationTo': {
equals: collectionConfig.slug,
},
},
{
'document.value': {
in: result.docs.map((doc) => doc.id),
},
},
],
},
})
const lockedDocs = Array.isArray(lockedDocuments?.docs) ? lockedDocuments.docs : []
result.docs = result.docs.map((doc) => {
const lockedDoc = lockedDocs.find((lock) => lock?.document?.value === doc.id)
return {
...doc,
isLocked: !!lockedDoc,
userEditing: lockedDoc ? lockedDoc._lastEdited?.user?.value : null,
}
})
} catch (error) {
result.docs = result.docs.map((doc) => ({
...doc,
isLocked: false,
userEditing: null,
}))
}
}
// /////////////////////////////////////
// beforeRead - Collection
// /////////////////////////////////////

View File

@@ -18,6 +18,7 @@ export type Arguments = {
disableErrors?: boolean
draft?: boolean
id: number | string
includeLockStatus?: boolean
overrideAccess?: boolean
req: PayloadRequest
showHiddenFields?: boolean
@@ -53,6 +54,7 @@ export const findByIDOperation = async <TSlug extends CollectionSlug>(
depth,
disableErrors,
draft: draftEnabled = false,
includeLockStatus,
overrideAccess = false,
req: { fallbackLocale, locale, t },
req,
@@ -99,6 +101,47 @@ export const findByIDOperation = async <TSlug extends CollectionSlug>(
return null
}
// /////////////////////////////////////
// Include Lock Status if required
// /////////////////////////////////////
if (includeLockStatus && id) {
let lockStatus = null
try {
const lockedDocument = await req.payload.find({
collection: 'payload-locked-documents',
depth: 1,
limit: 1,
pagination: false,
req,
where: {
and: [
{
'document.relationTo': {
equals: collectionConfig.slug,
},
},
{
'document.value': {
equals: id,
},
},
],
},
})
if (lockedDocument && lockedDocument.docs.length > 0) {
lockStatus = lockedDocument.docs[0]
}
} catch {
// swallow error
}
result.isLocked = !!lockStatus
result.userEditing = lockStatus?._lastEdited?.user?.value ?? null
}
// /////////////////////////////////////
// Replace document with draft if available
// /////////////////////////////////////

View File

@@ -18,6 +18,7 @@ export type Options<TSlug extends CollectionSlug> = {
disableErrors?: boolean
draft?: boolean
fallbackLocale?: TypedLocale
includeLockStatus?: boolean
limit?: number
locale?: 'all' | TypedLocale
overrideAccess?: boolean
@@ -40,6 +41,7 @@ export async function findLocal<TSlug extends CollectionSlug>(
depth,
disableErrors,
draft = false,
includeLockStatus,
limit,
overrideAccess = true,
page,
@@ -63,6 +65,7 @@ export async function findLocal<TSlug extends CollectionSlug>(
depth,
disableErrors,
draft,
includeLockStatus,
limit,
overrideAccess,
page,

View File

@@ -18,6 +18,7 @@ export type Options<TSlug extends CollectionSlug> = {
draft?: boolean
fallbackLocale?: TypedLocale
id: number | string
includeLockStatus?: boolean
locale?: 'all' | TypedLocale
overrideAccess?: boolean
req?: PayloadRequest
@@ -36,6 +37,7 @@ export default async function findByIDLocal<TSlug extends CollectionSlug>(
depth,
disableErrors = false,
draft = false,
includeLockStatus,
overrideAccess = true,
showHiddenFields,
} = options
@@ -55,6 +57,7 @@ export default async function findByIDLocal<TSlug extends CollectionSlug>(
depth,
disableErrors,
draft,
includeLockStatus,
overrideAccess,
req: await createLocalReq(options, payload),
showHiddenFields,

View File

@@ -25,6 +25,7 @@ import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles.js'
import { generateFileData } from '../../uploads/generateFileData.js'
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
import { uploadFiles } from '../../uploads/uploadFiles.js'
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
@@ -177,6 +178,17 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
}
try {
// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
const { lockedDocument, shouldUnlockDocument } = await checkDocumentLockStatus({
id,
collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
req,
})
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
@@ -323,6 +335,20 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
})
}
// /////////////////////////////////////
// Unlock the document if necessary
// /////////////////////////////////////
if (shouldUnlockDocument && lockedDocument) {
await payload.db.deleteOne({
collection: 'payload-locked-documents',
req,
where: {
id: { equals: lockedDocument.id },
},
})
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////

View File

@@ -26,6 +26,7 @@ import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles.js'
import { generateFileData } from '../../uploads/generateFileData.js'
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
import { uploadFiles } from '../../uploads/uploadFiles.js'
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
@@ -141,6 +142,17 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
throw new Forbidden(req.t)
}
// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
const { lockedDocument, shouldUnlockDocument } = await checkDocumentLockStatus({
id,
collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
req,
})
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
@@ -353,6 +365,20 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
})
}
// /////////////////////////////////////
// Unlock the document if necessary
// /////////////////////////////////////
if (shouldUnlockDocument && lockedDocument) {
await payload.db.deleteOne({
collection: 'payload-locked-documents',
req,
where: {
id: { equals: lockedDocument.id },
},
})
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////

View File

@@ -15,6 +15,7 @@ import { sanitizeCollection } from '../collections/config/sanitize.js'
import { migrationsCollection } from '../database/migrations/migrationsCollection.js'
import { InvalidConfiguration } from '../errors/index.js'
import { sanitizeGlobals } from '../globals/config/sanitize.js'
import { getLockedDocumentsCollection } from '../lockedDocuments/lockedDocumentsCollection.js'
import getPreferencesCollection from '../preferences/preferencesCollection.js'
import checkDuplicateCollections from '../utilities/checkDuplicateCollections.js'
import { defaults } from './defaults.js'
@@ -146,6 +147,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
config.i18n = i18nConfig
configWithDefaults.collections.push(getLockedDocumentsCollection(config as unknown as Config))
configWithDefaults.collections.push(getPreferencesCollection(config as unknown as Config))
configWithDefaults.collections.push(migrationsCollection)

View File

@@ -164,6 +164,15 @@ export type GlobalConfig = {
beforeValidate?: BeforeValidateHook[]
}
label?: Record<string, string> | string
/**
* Enables / Disables the ability to lock documents while editing
* @default true
*/
lockDocuments?:
| {
duration: number
}
| false
slug: string
/**
* Options used in typescript generation

View File

@@ -11,6 +11,7 @@ type Args = {
depth?: number
draft?: boolean
globalConfig: SanitizedGlobalConfig
includeLockStatus?: boolean
overrideAccess?: boolean
req: PayloadRequest
showHiddenFields?: boolean
@@ -25,6 +26,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
depth,
draft: draftEnabled = false,
globalConfig,
includeLockStatus,
overrideAccess = false,
req: { fallbackLocale, locale },
req,
@@ -56,6 +58,38 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
doc = {}
}
// /////////////////////////////////////
// Include Lock Status if required
// /////////////////////////////////////
if (includeLockStatus && slug) {
let lockStatus = null
try {
const lockedDocument = await req.payload.find({
collection: 'payload-locked-documents',
depth: 1,
limit: 1,
pagination: false,
req,
where: {
globalSlug: {
equals: slug,
},
},
})
if (lockedDocument && lockedDocument.docs.length > 0) {
lockStatus = lockedDocument.docs[0]
}
} catch {
// swallow error
}
doc.isLocked = !!lockStatus
doc.userEditing = lockStatus?._lastEdited?.user?.value ?? null
}
// /////////////////////////////////////
// Replace document with draft if available
// /////////////////////////////////////

View File

@@ -11,6 +11,7 @@ export type Options<TSlug extends GlobalSlug> = {
depth?: number
draft?: boolean
fallbackLocale?: TypedLocale
includeLockStatus?: boolean
locale?: 'all' | TypedLocale
overrideAccess?: boolean
req?: PayloadRequest
@@ -27,6 +28,7 @@ export default async function findOneLocal<TSlug extends GlobalSlug>(
slug: globalSlug,
depth,
draft = false,
includeLockStatus,
overrideAccess = true,
showHiddenFields,
} = options
@@ -42,6 +44,7 @@ export default async function findOneLocal<TSlug extends GlobalSlug>(
depth,
draft,
globalConfig,
includeLockStatus,
overrideAccess,
req: await createLocalReq(options, payload),
showHiddenFields,

View File

@@ -5,11 +5,13 @@ import type { Operation, PayloadRequest, Where } from '../../types/index.js'
import type { DataFromGlobalSlug, SanitizedGlobalConfig } from '../config/types.js'
import executeAccess from '../../auth/executeAccess.js'
import { APIError } from '../../errors/index.js'
import { afterChange } from '../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
import { deepCopyObjectSimple } from '../../index.js'
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
@@ -113,6 +115,16 @@ export const updateOperation = async <TSlug extends GlobalSlug>(
showHiddenFields,
})
// ///////////////////////////////////////////
// Handle potentially locked global documents
// ///////////////////////////////////////////
const { lockedDocument, shouldUnlockDocument } = await checkDocumentLockStatus({
globalSlug: slug,
lockErrorMessage: `Global with slug "${slug}" is currently locked by another user and cannot be updated.`,
req,
})
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
@@ -250,6 +262,22 @@ export const updateOperation = async <TSlug extends GlobalSlug>(
}
}
// /////////////////////////////////////
// Unlock the global if necessary
// /////////////////////////////////////
if (shouldUnlockDocument && lockedDocument) {
await payload.db.deleteOne({
collection: 'payload-locked-documents',
req,
where: {
globalSlug: {
equals: slug,
},
},
})
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////

View File

@@ -0,0 +1,45 @@
import type { CollectionConfig } from '../collections/config/types.js'
import type { Config } from '../config/types.js'
export const getLockedDocumentsCollection = (config: Config): CollectionConfig => ({
slug: 'payload-locked-documents',
admin: {
hidden: true,
},
fields: [
{
name: 'document',
type: 'relationship',
index: true,
maxDepth: 0,
relationTo: [...config.collections.map((collectionConfig) => collectionConfig.slug)],
},
{
name: 'globalSlug',
type: 'text',
},
{
name: '_lastEdited',
type: 'group',
fields: [
{
name: 'user',
type: 'relationship',
relationTo: config.collections
.filter((collectionConfig) => collectionConfig.auth)
.map((collectionConfig) => collectionConfig.slug),
required: true,
},
{
name: 'editedAt',
type: 'date',
},
],
},
{
name: 'isLocked',
type: 'checkbox',
defaultValue: false,
},
],
})

View File

@@ -0,0 +1,102 @@
import type { TypeWithID } from '../collections/config/types.js'
import type { PaginatedDocs } from '../database/types.js'
import type { JsonObject, PayloadRequest } from '../types/index.js'
import { APIError } from '../errors/index.js'
type CheckDocumentLockStatusArgs = {
collectionSlug?: string
globalSlug?: string
id?: number | string
lockDurationDefault?: number
lockErrorMessage?: string
req: PayloadRequest
}
type CheckDocumentLockResult = {
lockedDocument?: JsonObject & TypeWithID
shouldUnlockDocument: boolean
}
export const checkDocumentLockStatus = async ({
id,
collectionSlug,
globalSlug,
lockDurationDefault = 300, // Default 5 minutes in seconds
lockErrorMessage,
req,
}: CheckDocumentLockStatusArgs): Promise<CheckDocumentLockResult> => {
const { payload } = req
// Retrieve the lockDocuments property for either collection or global
const lockDocumentsProp = collectionSlug
? payload.config?.collections?.find((c) => c.slug === collectionSlug)?.lockDocuments
: payload.config?.globals?.find((g) => g.slug === globalSlug)?.lockDocuments
const isLockingEnabled = lockDocumentsProp !== false
// If lockDocuments is explicitly set to false, skip the lock logic and return early
if (isLockingEnabled === false) {
return { lockedDocument: undefined, shouldUnlockDocument: false }
}
let lockedDocumentQuery = {}
if (collectionSlug) {
lockedDocumentQuery = {
and: [
{ 'document.relationTo': { equals: collectionSlug } },
{ 'document.value': { equals: id } },
],
}
} else if (globalSlug) {
lockedDocumentQuery = { globalSlug: { equals: globalSlug } }
} else {
throw new Error('Either collectionSlug or globalSlug must be provided.')
}
const defaultLockErrorMessage = collectionSlug
? `Document with ID ${id} is currently locked by another user and cannot be modified.`
: `Global document with slug "${globalSlug}" is currently locked by another user and cannot be modified.`
const finalLockErrorMessage = lockErrorMessage || defaultLockErrorMessage
const lockedDocumentResult: PaginatedDocs<JsonObject & TypeWithID> = await payload.find({
collection: 'payload-locked-documents',
depth: 1,
limit: 1,
pagination: false,
req,
where: lockedDocumentQuery,
})
let shouldUnlockDocument = false
// If there's a locked document, check lock conditions
if (lockedDocumentResult.docs.length > 0) {
const lockedDoc = lockedDocumentResult.docs[0]
const lastEditedAt = new Date(lockedDoc?._lastEdited?.editedAt)
const now = new Date()
const lockDuration =
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
const lockDurationInMilliseconds = lockDuration * 1000
const currentUserId = req.user?.id
// If document is locked by another user and the lock hasn't expired
if (lockedDoc._lastEdited?.user?.value?.id !== currentUserId) {
if (now.getTime() - lastEditedAt.getTime() <= lockDurationInMilliseconds) {
throw new APIError(finalLockErrorMessage)
} else {
// If lock has expired, allow unlocking
shouldUnlockDocument = true
}
} else {
// If document is locked by the current user, allow unlocking
shouldUnlockDocument = true
}
}
return { lockedDocument: lockedDocumentResult.docs[0], shouldUnlockDocument }
}

View File

@@ -20,7 +20,7 @@ export type CollectionBeforeValidateHookWithArgs = (
collection?: CollectionConfig
pluginConfig?: StripePluginConfig
} & HookArgsWithCustomCollection,
) => void
) => Promise<Partial<any>>
export const createNewInStripe: CollectionBeforeValidateHookWithArgs = async (args) => {
const { collection, data, operation, pluginConfig, req } = args

View File

@@ -18,7 +18,7 @@ export type CollectionAfterDeleteHookWithArgs = (
collection?: CollectionConfig
pluginConfig?: StripePluginConfig
} & HookArgsWithCustomCollection,
) => void
) => Promise<void>
export const deleteFromStripe: CollectionAfterDeleteHookWithArgs = async (args) => {
const { collection, doc, pluginConfig, req } = args

View File

@@ -20,7 +20,7 @@ export type CollectionBeforeChangeHookWithArgs = (
collection?: CollectionConfig
pluginConfig?: StripePluginConfig
} & HookArgsWithCustomCollection,
) => void
) => Promise<Partial<any>>
export const syncExistingWithStripe: CollectionBeforeChangeHookWithArgs = async (args) => {
const { collection, data, operation, originalDoc, pluginConfig, req } = args

View File

@@ -9,7 +9,7 @@ type HandleCreatedOrUpdated = (
resourceType: string
syncConfig: SanitizedStripePluginConfig['sync'][0]
} & Parameters<StripeWebhookHandler>[0],
) => void
) => Promise<void>
export const handleCreatedOrUpdated: HandleCreatedOrUpdated = async (args) => {
const { config: payloadConfig, event, payload, pluginConfig, resourceType, syncConfig } = args

View File

@@ -5,7 +5,7 @@ type HandleDeleted = (
resourceType: string
syncConfig: SanitizedStripePluginConfig['sync'][0]
} & Parameters<StripeWebhookHandler>[0],
) => void
) => Promise<void>
export const handleDeleted: HandleDeleted = async (args) => {
const { event, payload, pluginConfig, resourceType, syncConfig } = args

View File

@@ -59,7 +59,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
// Field Schema
useEffect(() => {
const awaitInitialState = async () => {
const state = await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
@@ -89,7 +89,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
const formState = await getFormState({
const { state: formState } = await getFormState({
apiRoute: config.routes.api,
body: {
id,

View File

@@ -386,6 +386,7 @@ function $getAncestor(
predicate: (ancestor: LexicalNode) => boolean,
): LexicalNode | null {
let parent: LexicalNode | null = node
// eslint-disable-next-line no-empty
while (parent !== null && (parent = parent.getParent()) !== null && !predicate(parent)) {}
return parent
}

View File

@@ -46,7 +46,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
useEffect(() => {
const awaitInitialState = async () => {
const state = await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
@@ -65,7 +65,7 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
return await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
@@ -75,6 +75,8 @@ export const DrawerContent: React.FC<Omit<FieldsDrawerProps, 'drawerSlug' | 'dra
},
serverURL: config.serverURL,
})
return state
},
[config.routes.api, config.serverURL, schemaFieldsPath, id],

View File

@@ -90,7 +90,7 @@ export const LinkButton: React.FC = () => {
text: editor.selection ? Editor.string(editor, editor.selection) : '',
}
const state = await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
data,

View File

@@ -100,7 +100,7 @@ export const LinkElement = () => {
url: element.url,
}
const state = await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
data,

View File

@@ -38,7 +38,7 @@ export const LinkDrawer: React.FC<Props> = ({
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
return await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
@@ -48,6 +48,8 @@ export const LinkDrawer: React.FC<Props> = ({
},
serverURL: config.serverURL,
})
return state
},
[config.routes.api, config.serverURL, fieldMapPath, id],

View File

@@ -71,7 +71,7 @@ export const UploadDrawer: React.FC<{
const data = deepCopyObject(element?.fields || {})
const awaitInitialState = async () => {
const state = await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
@@ -101,7 +101,7 @@ export const UploadDrawer: React.FC<{
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
return await getFormState({
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
id,
@@ -111,6 +111,8 @@ export const UploadDrawer: React.FC<{
},
serverURL: config.serverURL,
})
return state
},
[config.routes.api, config.serverURL, relatedCollection.slug, schemaPath, id],

View File

@@ -126,6 +126,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:addFilter',
'general:adminTheme',
'general:and',
'general:anotherUserTakenOver',
'general:applyChanges',
'general:ascending',
'general:automatic',
@@ -150,6 +151,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:createNewLabel',
'general:creating',
'general:creatingNewLabel',
'general:currentlyEditing',
'general:custom',
'general:dark',
'general:dashboard',
@@ -161,13 +163,16 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:depth',
'general:deselectAllRows',
'general:document',
'general:documentLocked',
'general:documents',
'general:duplicate',
'general:duplicateWithoutSaving',
'general:edit',
'general:editing',
'general:editingLabel',
'general:editingTakenOver',
'general:editLabel',
'general:editedSince',
'general:email',
'general:emailAddress',
'general:enterAValue',
@@ -178,6 +183,8 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:filters',
'general:filterWhere',
'general:globals',
'general:goBack',
'general:isEditing',
'general:language',
'general:lastModified',
'general:leaveAnyway',
@@ -230,6 +237,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:success',
'general:successfullyCreated',
'general:successfullyDuplicated',
'general:takeOver',
'general:thisLanguage',
'general:titleDeleted',
'general:true',
@@ -243,6 +251,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:updatedCountSuccessfully',
'general:updatedSuccessfully',
'general:updating',
'general:viewReadOnly',
'general:uploading',
'general:welcome',

View File

@@ -177,6 +177,7 @@ export const arTranslations: DefaultTranslationsObject = {
addFilter: 'أضف فلتر',
adminTheme: 'شكل واجهة المستخدم',
and: 'و',
anotherUserTakenOver: 'قام مستخدم آخر بالاستيلاء على تحرير هذا المستند.',
applyChanges: 'طبق التغييرات',
ascending: 'تصاعدي',
automatic: 'تلقائي',
@@ -201,6 +202,8 @@ export const arTranslations: DefaultTranslationsObject = {
createNewLabel: 'إنشاء {{label}} جديد',
creating: 'يتمّ الإنشاء',
creatingNewLabel: 'جاري إنشاء {{label}} جديد',
currentlyEditing:
'يقوم حاليًا بتحرير هذا المستند. إذا توليت، سيتم منعه من الاستمرار في التحرير وقد يفقد التغييرات غير المحفوظة.',
custom: 'مخصص',
dark: 'غامق',
dashboard: 'لوحة التّحكّم',
@@ -212,14 +215,17 @@ export const arTranslations: DefaultTranslationsObject = {
descending: 'تنازلي',
deselectAllRows: 'إلغاء تحديد جميع الصفوف',
document: 'وثيقة',
documentLocked: 'تم قفل المستند',
documents: 'وثائق',
duplicate: 'استنساخ',
duplicateWithoutSaving: 'استنساخ بدون حفظ التغييرات',
edit: 'تعديل',
editedSince: 'تم التحرير منذ',
editing: 'جاري التعديل',
editingLabel_many: 'تعديل {{count}} {{label}}',
editingLabel_one: 'تعديل {{count}} {{label}}',
editingLabel_other: 'تعديل {{count}} {{label}}',
editingTakenOver: 'تم الاستيلاء على التحرير',
editLabel: 'تعديل {{label}}',
email: 'البريد الإلكتروني',
emailAddress: 'عنوان البريد الإلكتروني',
@@ -232,6 +238,8 @@ export const arTranslations: DefaultTranslationsObject = {
filters: 'عوامل التصفية',
filterWhere: 'تصفية {{label}} حيث',
globals: 'عامة',
goBack: 'العودة',
isEditing: 'يحرر',
language: 'اللغة',
lastModified: 'آخر تعديل',
leaveAnyway: 'المغادرة على أي حال',
@@ -287,6 +295,7 @@ export const arTranslations: DefaultTranslationsObject = {
success: 'النجاح',
successfullyCreated: '{{label}} تم إنشاؤها بنجاح.',
successfullyDuplicated: '{{label}} تم استنساخها بنجاح.',
takeOver: 'تولي',
thisLanguage: 'العربية',
titleDeleted: 'تم حذف {{label}} "{{title}}" بنجاح.',
true: 'صحيح',
@@ -302,6 +311,7 @@ export const arTranslations: DefaultTranslationsObject = {
username: 'اسم المستخدم',
users: 'المستخدمين',
value: 'القيمة',
viewReadOnly: 'عرض للقراءة فقط',
welcome: 'مرحبًا',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const azTranslations: DefaultTranslationsObject = {
addFilter: 'Filter əlavə et',
adminTheme: 'Admin Mövzusu',
and: 'Və',
anotherUserTakenOver: 'Başqa bir istifadəçi bu sənədin redaktəsini ələ keçirdi.',
applyChanges: 'Dəyişiklikləri Tətbiq Edin',
ascending: 'Artan',
automatic: 'Avtomatik',
@@ -203,6 +204,8 @@ export const azTranslations: DefaultTranslationsObject = {
createNewLabel: 'Yeni {{label}} yarat',
creating: 'Yaradılır',
creatingNewLabel: 'Yeni {{label}} yaradılır',
currentlyEditing:
'hazırda bu sənədi redaktə edir. Siz öhdəliyi götürsəniz, redaktəni davam etdirməkdən bloklanacaqlar və qeydə alınmamış dəyişiklikləri itirə bilərlər.',
custom: 'Xüsusi',
dark: 'Tünd',
dashboard: 'Panel',
@@ -214,14 +217,17 @@ export const azTranslations: DefaultTranslationsObject = {
descending: 'Azalan',
deselectAllRows: 'Bütün sıraları seçimi ləğv edin',
document: 'Sənəd',
documentLocked: 'Sənəd kilidləndi',
documents: 'Sənədlər',
duplicate: 'Dublikat',
duplicateWithoutSaving: 'Dəyişiklikləri saxlamadan dublikatla',
edit: 'Redaktə et',
editedSince: 'Redaktə edilib',
editing: 'Redaktə olunur',
editingLabel_many: '{{count}} {{label}} redaktə olunur',
editingLabel_one: '{{count}} {{label}} redaktə olunur',
editingLabel_other: '{{count}} {{label}} redaktə olunur',
editingTakenOver: 'Redaktə ələ keçirildi',
editLabel: '{{label}} redaktə et',
email: 'Elektron poçt',
emailAddress: 'Elektron poçt ünvanı',
@@ -234,6 +240,8 @@ export const azTranslations: DefaultTranslationsObject = {
filters: 'Filtərlər',
filterWhere: '{{label}} filtrlə',
globals: 'Qloballar',
goBack: 'Geri qayıt',
isEditing: 'redaktə edir',
language: 'Dil',
lastModified: 'Son dəyişdirildi',
leaveAnyway: 'Heç olmasa çıx',
@@ -289,6 +297,7 @@ export const azTranslations: DefaultTranslationsObject = {
success: 'Uğur',
successfullyCreated: '{{label}} uğurla yaradıldı.',
successfullyDuplicated: '{{label}} uğurla dublikatlandı.',
takeOver: 'Əvvəl',
thisLanguage: 'Azərbaycan dili',
titleDeleted: '{{label}} "{{title}}" uğurla silindi.',
true: 'Doğru',
@@ -305,6 +314,7 @@ export const azTranslations: DefaultTranslationsObject = {
username: 'İstifadəçi adı',
users: 'İstifadəçilər',
value: 'Dəyər',
viewReadOnly: 'Yalnız oxu rejimində bax',
welcome: 'Xoş gəldiniz',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const bgTranslations: DefaultTranslationsObject = {
addFilter: 'Добави филтър',
adminTheme: 'Цветова тема',
and: 'И',
anotherUserTakenOver: 'Друг потребител пое редактирането на този документ.',
applyChanges: 'Приложи промените',
ascending: 'Възходящ',
automatic: 'Автоматична',
@@ -202,6 +203,8 @@ export const bgTranslations: DefaultTranslationsObject = {
createNewLabel: 'Създай нов {{label}}',
creating: 'Създава се',
creatingNewLabel: 'Създаване на нов {{label}}',
currentlyEditing:
'в момента редактира този документ. Ако поемете управлението, те ще бъдат блокирани от продължаване на редактирането и може да загубят незаписаните промени.',
custom: 'Персонализиран',
dark: 'Тъмна',
dashboard: 'Табло',
@@ -213,14 +216,17 @@ export const bgTranslations: DefaultTranslationsObject = {
descending: 'Низходящо',
deselectAllRows: 'Деселектирай всички редове',
document: 'Документ',
documentLocked: 'Документът е заключен',
documents: 'Документи',
duplicate: 'Дупликирай',
duplicateWithoutSaving: 'Дупликирай без да запазваш промените',
edit: 'Редактирай',
editedSince: 'Редактирано от',
editing: 'Редактиране',
editingLabel_many: 'Редактиране на {{count}} {{label}}',
editingLabel_one: 'Редактиране на {{count}} {{label}}',
editingLabel_other: 'Редактиране на {{count}} {{label}}',
editingTakenOver: 'Редактирането е поето',
editLabel: 'Редактирай {{label}}',
email: 'Имейл',
emailAddress: 'Имейл адрес',
@@ -233,6 +239,8 @@ export const bgTranslations: DefaultTranslationsObject = {
filters: 'Филтри',
filterWhere: 'Филтрирай {{label}} където',
globals: 'Глобални',
goBack: 'Върни се',
isEditing: 'редактира',
language: 'Език',
lastModified: 'Последно променено',
leaveAnyway: 'Напусни въпреки това',
@@ -288,6 +296,7 @@ export const bgTranslations: DefaultTranslationsObject = {
success: 'Успех',
successfullyCreated: '{{label}} успешно създаден.',
successfullyDuplicated: '{{label}} успешно дупликиран.',
takeOver: 'Поемане',
thisLanguage: 'Български',
titleDeleted: '{{label}} "{{title}}" успешно изтрит.',
true: 'Истина',
@@ -303,6 +312,7 @@ export const bgTranslations: DefaultTranslationsObject = {
username: 'Потребителско име',
users: 'Потребители',
value: 'Стойност',
viewReadOnly: 'Преглед само за четене',
welcome: 'Добре дошъл',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const csTranslations: DefaultTranslationsObject = {
addFilter: 'Přidat filtr',
adminTheme: 'Motiv administračního rozhraní',
and: 'a',
anotherUserTakenOver: 'Jiný uživatel převzal úpravy tohoto dokumentu.',
applyChanges: 'Použít změny',
ascending: 'Vzestupně',
automatic: 'Automatický',
@@ -202,6 +203,8 @@ export const csTranslations: DefaultTranslationsObject = {
createNewLabel: 'Vytvořit nový {{label}}',
creating: 'Vytváření',
creatingNewLabel: 'Vytváření nového {{label}}',
currentlyEditing:
'právě upravuje tento dokument. Pokud převezmete kontrolu, budou zablokováni v pokračování úprav a mohou také přijít o neuložené změny.',
custom: 'Vlastní',
dark: 'Tmavý',
dashboard: 'Nástěnka',
@@ -213,14 +216,17 @@ export const csTranslations: DefaultTranslationsObject = {
descending: 'Sestupně',
deselectAllRows: 'Zrušte výběr všech řádků',
document: 'Dokument',
documentLocked: 'Dokument je uzamčen',
documents: 'Dokumenty',
duplicate: 'Duplikovat',
duplicateWithoutSaving: 'Duplikovat bez uložení změn',
edit: 'Upravit',
editedSince: 'Upraveno od',
editing: 'Úprava',
editingLabel_many: 'Úprava {{count}} {{label}}',
editingLabel_one: 'Úprava {{count}} {{label}}',
editingLabel_other: 'Úprava {{count}} {{label}}',
editingTakenOver: 'Úpravy byly převzaty',
editLabel: 'Upravit {{label}}',
email: 'E-mail',
emailAddress: 'E-mailová adresa',
@@ -233,6 +239,8 @@ export const csTranslations: DefaultTranslationsObject = {
filters: 'Filtry',
filterWhere: 'Filtrovat {{label}} kde',
globals: 'Globální',
goBack: 'Vrátit se',
isEditing: 'upravuje',
language: 'Jazyk',
lastModified: 'Naposledy změněno',
leaveAnyway: 'Přesto odejít',
@@ -288,6 +296,7 @@ export const csTranslations: DefaultTranslationsObject = {
success: 'Úspěch',
successfullyCreated: '{{label}} úspěšně vytvořeno.',
successfullyDuplicated: '{{label}} úspěšně duplikováno.',
takeOver: 'Převzít',
thisLanguage: 'Čeština',
titleDeleted: '{{label}} "{{title}}" úspěšně smazáno.',
true: 'Pravda',
@@ -303,6 +312,7 @@ export const csTranslations: DefaultTranslationsObject = {
username: 'Uživatelské jméno',
users: 'Uživatelé',
value: 'Hodnota',
viewReadOnly: 'Zobrazit pouze pro čtení',
welcome: 'Vítejte',
},
operators: {

View File

@@ -182,6 +182,7 @@ export const deTranslations: DefaultTranslationsObject = {
addFilter: 'Filter hinzufügen',
adminTheme: 'Admin-Farbthema',
and: 'Und',
anotherUserTakenOver: 'Ein anderer Benutzer hat die Bearbeitung dieses Dokuments übernommen.',
applyChanges: 'Änderungen anwenden',
ascending: 'Aufsteigend',
automatic: 'Automatisch',
@@ -207,6 +208,8 @@ export const deTranslations: DefaultTranslationsObject = {
createNewLabel: '{{label}} neu erstellen',
creating: 'Erstelle',
creatingNewLabel: 'Erstelle {{label}}',
currentlyEditing:
'bearbeitet gerade dieses Dokument. Wenn Sie übernehmen, wird die Bearbeitung blockiert und nicht gespeicherte Änderungen können verloren gehen.',
custom: 'Benutzerdefiniert',
dark: 'Dunkel',
dashboard: 'Übersicht',
@@ -218,14 +221,17 @@ export const deTranslations: DefaultTranslationsObject = {
descending: 'Absteigend',
deselectAllRows: 'Alle Zeilen abwählen',
document: 'Dokument',
documentLocked: 'Dokument gesperrt',
documents: 'Dokumente',
duplicate: 'Duplizieren',
duplicateWithoutSaving: 'Dupliziere ohne Änderungen zu speichern',
edit: 'Bearbeiten',
editedSince: 'Bearbeitet seit',
editing: 'Bearbeite',
editingLabel_many: 'Bearbeiten von {{count}} {{label}}',
editingLabel_one: 'Bearbeiten von {{count}} {{label}}',
editingLabel_other: 'Bearbeiten von {{count}} {{label}}',
editingTakenOver: 'Bearbeitung übernommen',
editLabel: '{{label}} bearbeiten',
email: 'E-Mail',
emailAddress: 'E-Mail-Adresse',
@@ -238,6 +244,8 @@ export const deTranslations: DefaultTranslationsObject = {
filters: 'Filter',
filterWhere: 'Filter {{label}} wo',
globals: 'Globale Dokumente',
goBack: 'Zurück',
isEditing: 'bearbeitet',
language: 'Sprache',
lastModified: 'Zuletzt geändert',
leaveAnyway: 'Trotzdem verlassen',
@@ -293,6 +301,7 @@ export const deTranslations: DefaultTranslationsObject = {
success: 'Erfolg',
successfullyCreated: '{{label}} erfolgreich erstellt.',
successfullyDuplicated: '{{label}} wurde erfolgreich dupliziert.',
takeOver: 'Übernehmen',
thisLanguage: 'Deutsch',
titleDeleted: '{{label}} {{title}} wurde erfolgreich gelöscht.',
true: 'Wahr',
@@ -309,6 +318,7 @@ export const deTranslations: DefaultTranslationsObject = {
username: 'Benutzername',
users: 'Benutzer',
value: 'Wert',
viewReadOnly: 'Nur-Lese-Ansicht',
welcome: 'Willkommen',
},
operators: {

View File

@@ -180,6 +180,7 @@ export const enTranslations = {
addFilter: 'Add Filter',
adminTheme: 'Admin Theme',
and: 'And',
anotherUserTakenOver: 'Another user has taken over editing this document.',
applyChanges: 'Apply Changes',
ascending: 'Ascending',
automatic: 'Automatic',
@@ -205,6 +206,8 @@ export const enTranslations = {
createNewLabel: 'Create new {{label}}',
creating: 'Creating',
creatingNewLabel: 'Creating new {{label}}',
currentlyEditing:
'is currently editing this document. If you take over, they will be blocked from continuing to edit, and may also lose unsaved changes.',
custom: 'Custom',
dark: 'Dark',
dashboard: 'Dashboard',
@@ -216,14 +219,17 @@ export const enTranslations = {
descending: 'Descending',
deselectAllRows: 'Deselect all rows',
document: 'Document',
documentLocked: 'Document locked',
documents: 'Documents',
duplicate: 'Duplicate',
duplicateWithoutSaving: 'Duplicate without saving changes',
edit: 'Edit',
editedSince: 'Edited since',
editing: 'Editing',
editingLabel_many: 'Editing {{count}} {{label}}',
editingLabel_one: 'Editing {{count}} {{label}}',
editingLabel_other: 'Editing {{count}} {{label}}',
editingTakenOver: 'Editing taken over',
editLabel: 'Edit {{label}}',
email: 'Email',
emailAddress: 'Email Address',
@@ -236,6 +242,8 @@ export const enTranslations = {
filters: 'Filters',
filterWhere: 'Filter {{label}} where',
globals: 'Globals',
goBack: 'Go back',
isEditing: 'is editing',
language: 'Language',
lastModified: 'Last Modified',
leaveAnyway: 'Leave anyway',
@@ -291,6 +299,7 @@ export const enTranslations = {
success: 'Success',
successfullyCreated: '{{label}} successfully created.',
successfullyDuplicated: '{{label}} successfully duplicated.',
takeOver: 'Take over',
thisLanguage: 'English',
titleDeleted: '{{label}} "{{title}}" successfully deleted.',
true: 'True',
@@ -306,6 +315,7 @@ export const enTranslations = {
username: 'Username',
users: 'Users',
value: 'Value',
viewReadOnly: 'View read-only',
welcome: 'Welcome',
},
operators: {

View File

@@ -182,6 +182,7 @@ export const esTranslations: DefaultTranslationsObject = {
addFilter: 'Añadir filtro',
adminTheme: 'Tema del admin',
and: 'Y',
anotherUserTakenOver: 'Otro usuario ha tomado el control de la edición de este documento.',
applyChanges: 'Aplicar Cambios',
ascending: 'Ascendente',
automatic: 'Automático',
@@ -207,6 +208,8 @@ export const esTranslations: DefaultTranslationsObject = {
createNewLabel: 'Crear nuevo {{label}}',
creating: 'Creando',
creatingNewLabel: 'Creando nuevo {{label}}',
currentlyEditing:
'está editando este documento. Si tomas el control, se les bloqueará para que no continúen editando y podrían perder los cambios no guardados.',
custom: 'Personalizado',
dark: 'Oscuro',
dashboard: 'Tablero',
@@ -218,14 +221,17 @@ export const esTranslations: DefaultTranslationsObject = {
descending: 'Descendente',
deselectAllRows: 'Deselecciona todas las filas',
document: 'Documento',
documentLocked: 'Documento bloqueado',
documents: 'Documentos',
duplicate: 'Duplicar',
duplicateWithoutSaving: 'Duplicar sin guardar cambios',
edit: 'Editar',
editedSince: 'Editado desde',
editing: 'Editando',
editingLabel_many: 'Edición de {{count}} {{label}}',
editingLabel_one: 'Editando {{count}} {{label}}',
editingLabel_other: 'Edición de {{count}} {{label}}',
editingTakenOver: 'Edición tomada',
editLabel: 'Editar {{label}}',
email: 'Correo electrónico',
emailAddress: 'Dirección de Correo Electrónico',
@@ -238,6 +244,8 @@ export const esTranslations: DefaultTranslationsObject = {
filters: 'Filtros',
filterWhere: 'Filtrar {{label}} donde',
globals: 'Globales',
goBack: 'Volver',
isEditing: 'está editando',
language: 'Idioma',
lastModified: 'Última modificación',
leaveAnyway: 'Salir de todos modos',
@@ -293,6 +301,7 @@ export const esTranslations: DefaultTranslationsObject = {
success: 'Éxito',
successfullyCreated: '{{label}} creado correctamente.',
successfullyDuplicated: '{{label}} duplicado correctamente.',
takeOver: 'Tomar el control',
thisLanguage: 'Español',
titleDeleted: '{{label}} {{title}} eliminado correctamente.',
true: 'Verdadero',
@@ -308,6 +317,7 @@ export const esTranslations: DefaultTranslationsObject = {
username: 'Nombre de usuario',
users: 'Usuarios',
value: 'Valor',
viewReadOnly: 'Ver solo lectura',
welcome: 'Bienvenido',
},
operators: {

View File

@@ -177,6 +177,7 @@ export const faTranslations: DefaultTranslationsObject = {
addFilter: 'افزودن علامت',
adminTheme: 'پوسته پیشخوان',
and: 'و',
anotherUserTakenOver: 'کاربر دیگری ویرایش این سند را به دست گرفته است.',
applyChanges: 'اعمال تغییرات',
ascending: 'صعودی',
automatic: 'خودکار',
@@ -202,6 +203,8 @@ export const faTranslations: DefaultTranslationsObject = {
createNewLabel: 'ساختن {{label}} تازه',
creating: 'در حال ساخت',
creatingNewLabel: 'در حال ساختن {{label}} تازه',
currentlyEditing:
'در حال حاضر در حال ویرایش این سند است. اگر شما مسئولیت را به عهده بگیرید، از ادامه ویرایش مسدود خواهد شد و ممکن است تغییرات ذخیره نشده را از دست بدهند.',
custom: 'سفارشی',
dark: 'تاریک',
dashboard: 'پیشخوان',
@@ -213,14 +216,17 @@ export const faTranslations: DefaultTranslationsObject = {
descending: 'رو به پایین',
deselectAllRows: 'تمام سطرها را از انتخاب خارج کنید',
document: 'سند',
documentLocked: 'سند قفل شده است',
documents: 'اسناد',
duplicate: 'تکراری',
duplicateWithoutSaving: 'رونوشت بدون ذخیره کردن تغییرات',
edit: 'نگارش',
editedSince: 'ویرایش شده از',
editing: 'در حال نگارش',
editingLabel_many: 'در حال نگارش {{count}} از {{label}}',
editingLabel_one: 'در حال نگارش {{count}} از {{label}}',
editingLabel_other: 'در حال نگارش {{count}} از {{label}}',
editingTakenOver: 'ویرایش به دست گرفته شد',
editLabel: 'نگارش {{label}}',
email: 'رایانامه',
emailAddress: 'نشانی رایانامه',
@@ -233,6 +239,8 @@ export const faTranslations: DefaultTranslationsObject = {
filters: 'علامت‌گذاری‌ها',
filterWhere: 'علامت گذاری کردن {{label}} جایی که',
globals: 'سراسری',
goBack: 'برگشت',
isEditing: 'در حال ویرایش است',
language: 'زبان',
lastModified: 'آخرین نگارش',
leaveAnyway: 'به هر حال ترک کن',
@@ -288,6 +296,7 @@ export const faTranslations: DefaultTranslationsObject = {
success: 'موفقیت',
successfullyCreated: '{{label}} با موفقیت ساخته شد.',
successfullyDuplicated: '{{label}} با موفقیت رونوشت شد.',
takeOver: 'تحویل گرفتن',
thisLanguage: 'فارسی',
titleDeleted: '{{label}} "{{title}}" با موفقیت پاک شد.',
true: 'درست',
@@ -303,6 +312,7 @@ export const faTranslations: DefaultTranslationsObject = {
username: 'نام کاربری',
users: 'کاربران',
value: 'مقدار',
viewReadOnly: 'فقط برای خواندن مشاهده کنید',
welcome: 'خوش‌آمدید',
},
operators: {

View File

@@ -185,6 +185,7 @@ export const frTranslations: DefaultTranslationsObject = {
addFilter: 'Ajouter un filtre',
adminTheme: 'Thème dadministration',
and: 'Et',
anotherUserTakenOver: 'Un autre utilisateur a pris en charge la modification de ce document.',
applyChanges: 'Appliquer les modifications',
ascending: 'Ascendant',
automatic: 'Automatique',
@@ -210,6 +211,8 @@ export const frTranslations: DefaultTranslationsObject = {
createNewLabel: 'Créer un(e) nouveau ou nouvelle {{label}}',
creating: 'création en cours',
creatingNewLabel: 'Création dun(e) nouveau ou nouvelle {{label}}',
currentlyEditing:
'est en train de modifier ce document. Si vous prenez le contrôle, ils seront bloqués pour continuer à modifier et pourraient également perdre les modifications non enregistrées.',
custom: 'Personnalisé',
dark: 'Sombre',
dashboard: 'Tableau de bord',
@@ -221,14 +224,17 @@ export const frTranslations: DefaultTranslationsObject = {
descending: 'Descendant(e)',
deselectAllRows: 'Désélectionner toutes les lignes',
document: 'Document',
documentLocked: 'Document verrouillé',
documents: 'Documents',
duplicate: 'Dupliquer',
duplicateWithoutSaving: 'Dupliquer sans enregistrer les modifications',
edit: 'Éditer',
editedSince: 'Modifié depuis',
editing: 'Modification en cours',
editingLabel_many: 'Modification des {{count}} {{label}}',
editingLabel_one: 'Modification de {{count}} {{label}}',
editingLabel_other: 'Modification des {{count}} {{label}}',
editingTakenOver: 'Modification prise en charge',
editLabel: 'Modifier {{label}}',
email: 'E-mail',
emailAddress: 'Adresse e-mail',
@@ -241,6 +247,8 @@ export const frTranslations: DefaultTranslationsObject = {
filters: 'Filtres',
filterWhere: 'Filtrer {{label}} où',
globals: 'Globals(es)',
goBack: 'Retourner',
isEditing: 'est en train de modifier',
language: 'Langue',
lastModified: 'Dernière modification',
leaveAnyway: 'Quitter quand même',
@@ -296,6 +304,7 @@ export const frTranslations: DefaultTranslationsObject = {
success: 'Succès',
successfullyCreated: '{{label}} créé(e) avec succès.',
successfullyDuplicated: '{{label}} dupliqué(e) avec succès.',
takeOver: 'Prendre en charge',
thisLanguage: 'Français',
titleDeleted: '{{label}} "{{title}}" supprimé(e) avec succès.',
true: 'Vrai',
@@ -312,6 +321,7 @@ export const frTranslations: DefaultTranslationsObject = {
username: "Nom d'utilisateur",
users: 'Utilisateurs',
value: 'Valeur',
viewReadOnly: 'Afficher en lecture seule',
welcome: 'Bienvenue',
},
operators: {

View File

@@ -174,6 +174,7 @@ export const heTranslations: DefaultTranslationsObject = {
addFilter: 'הוסף מסנן',
adminTheme: 'ערכת נושא ממשק הניהול',
and: 'וגם',
anotherUserTakenOver: 'משתמש אחר השתלט על עריכת מסמך זה.',
applyChanges: 'החל שינויים',
ascending: 'בסדר עולה',
automatic: 'אוטומטי',
@@ -198,6 +199,8 @@ export const heTranslations: DefaultTranslationsObject = {
createNewLabel: 'יצירת {{label}} חדש',
creating: 'יצירה',
creatingNewLabel: 'יצירת {{label}} חדש',
currentlyEditing:
'עורך כעת את המסמך הזה. אם תשתלט, הם ייחסמו מהמשך העריכה וייתכן שגם יאבדו שינויים שלא נשמרו.',
custom: 'מותאם אישית',
dark: 'כהה',
dashboard: 'לוח מחוונים',
@@ -209,14 +212,17 @@ export const heTranslations: DefaultTranslationsObject = {
descending: 'בסדר יורד',
deselectAllRows: 'בטל בחירת כל השורות',
document: 'מסמך',
documentLocked: 'המסמך ננעל',
documents: 'מסמכים',
duplicate: 'שכפול',
duplicateWithoutSaving: 'שכפול ללא שמירת שינויים',
edit: 'עריכה',
editedSince: 'נערך מאז',
editing: 'עריכה',
editingLabel_many: 'עריכת {{count}} {{label}}',
editingLabel_one: 'עריכת {{label}} אחד',
editingLabel_other: 'עריכת {{count}} {{label}}',
editingTakenOver: 'העריכה נלקחה על ידי',
editLabel: 'עריכת {{label}}',
email: 'דוא"ל',
emailAddress: 'כתובת דוא"ל',
@@ -229,6 +235,8 @@ export const heTranslations: DefaultTranslationsObject = {
filters: 'מסננים',
filterWhere: 'סנן {{label}} בהם',
globals: 'גלובלים',
goBack: 'חזור',
isEditing: 'עורך',
language: 'שפה',
lastModified: 'נערך לאחרונה',
leaveAnyway: 'צא בכל זאת',
@@ -283,6 +291,7 @@ export const heTranslations: DefaultTranslationsObject = {
success: 'הצלחה',
successfullyCreated: '{{label}} נוצר בהצלחה.',
successfullyDuplicated: '{{label}} שוכפל בהצלחה.',
takeOver: 'קח פיקוד',
thisLanguage: 'עברית',
titleDeleted: '{{label}} "{{title}}" נמחק בהצלחה.',
true: 'True',
@@ -298,6 +307,7 @@ export const heTranslations: DefaultTranslationsObject = {
username: 'שם משתמש',
users: 'משתמשים',
value: 'ערך',
viewReadOnly: 'הצג קריאה בלבד',
welcome: 'ברוך הבא',
},
operators: {

View File

@@ -179,6 +179,7 @@ export const hrTranslations: DefaultTranslationsObject = {
addFilter: 'Dodaj filter',
adminTheme: 'Administratorska tema',
and: 'I',
anotherUserTakenOver: 'Drugi korisnik je preuzeo uređivanje ovog dokumenta.',
applyChanges: 'Primijeni promjene',
ascending: 'Uzlazno',
automatic: 'Automatsko',
@@ -203,6 +204,8 @@ export const hrTranslations: DefaultTranslationsObject = {
createNewLabel: 'Kreiraj novo {{label}}',
creating: 'Kreira se',
creatingNewLabel: 'Kreiranje novog {{label}}',
currentlyEditing:
'trenutno uređuje ovaj dokument. Ako preuzmete, bit će im onemogućeno daljnje uređivanje i mogu izgubiti nespremljene promjene.',
custom: 'Prilagođen',
dark: 'Tamno',
dashboard: 'Nadzorna ploča',
@@ -214,14 +217,17 @@ export const hrTranslations: DefaultTranslationsObject = {
descending: 'Silazno',
deselectAllRows: 'Odznači sve redove',
document: 'Dokument',
documentLocked: 'Dokument je zaključan',
documents: 'Dokumenti',
duplicate: 'Duplikat',
duplicateWithoutSaving: 'Dupliciraj bez spremanja promjena',
edit: 'Uredi',
editedSince: 'Uređeno od',
editing: 'Uređivanje',
editingLabel_many: 'Uređivanje {{count}} {{label}}',
editingLabel_one: 'Uređivanje {{count}} {{label}}',
editingLabel_other: 'Uređivanje {{count}} {{label}}',
editingTakenOver: 'Uređivanje preuzeto',
editLabel: 'Uredi {{label}}',
email: 'Email',
emailAddress: 'Email adresa',
@@ -234,6 +240,8 @@ export const hrTranslations: DefaultTranslationsObject = {
filters: 'Filteri',
filterWhere: 'Filter {{label}} gdje',
globals: 'Globali',
goBack: 'Vrati se',
isEditing: 'uređuje',
language: 'Jezik',
lastModified: 'Zadnja promjena',
leaveAnyway: 'Svejedno napusti',
@@ -289,6 +297,7 @@ export const hrTranslations: DefaultTranslationsObject = {
success: 'Uspjeh',
successfullyCreated: '{{label}} uspješno kreirano.',
successfullyDuplicated: '{{label}} uspješno duplicirano.',
takeOver: 'Preuzmi',
thisLanguage: 'Hrvatski',
titleDeleted: '{{label}} "{{title}}" uspješno obrisano.',
true: 'Istinito',
@@ -304,6 +313,7 @@ export const hrTranslations: DefaultTranslationsObject = {
username: 'Korisničko ime',
users: 'Korisnici',
value: 'Attribute',
viewReadOnly: 'Pogledaj samo za čitanje',
welcome: 'Dobrodošli',
},
operators: {

View File

@@ -180,6 +180,7 @@ export const huTranslations: DefaultTranslationsObject = {
addFilter: 'Szűrő hozzáadása',
adminTheme: 'Admin téma',
and: 'És',
anotherUserTakenOver: 'Egy másik felhasználó átvette ennek a dokumentumnak a szerkesztését.',
applyChanges: 'Változtatások alkalmazása',
ascending: 'Növekvő',
automatic: 'Automatikus',
@@ -205,6 +206,8 @@ export const huTranslations: DefaultTranslationsObject = {
createNewLabel: 'Új {{label}} létrehozása',
creating: 'Létrehozás',
creatingNewLabel: 'Új {{label}} létrehozása',
currentlyEditing:
'jelenleg szerkeszti ezt a dokumentumot. Ha átveszed, nem tudja folytatni a szerkesztést, és elveszítheti a mentetlen módosításokat.',
custom: 'Egyéni',
dark: 'Sötét',
dashboard: 'Irányítópult',
@@ -216,14 +219,17 @@ export const huTranslations: DefaultTranslationsObject = {
descending: 'Csökkenő',
deselectAllRows: 'Jelölje ki az összes sort',
document: 'Dokumentum',
documentLocked: 'A dokumentum zárolva van',
documents: 'Dokumentumok',
duplicate: 'Duplikálás',
duplicateWithoutSaving: 'Duplikálás a módosítások mentése nélkül',
edit: 'Szerkesztés',
editedSince: 'Szerkesztve',
editing: 'Szerkesztés',
editingLabel_many: '{{count}} {{label}} szerkesztése',
editingLabel_one: '{{count}} {{label}} szerkesztése',
editingLabel_other: '{{count}} {{label}} szerkesztése',
editingTakenOver: 'A szerkesztést átvették',
editLabel: '{{label}} szerkesztése',
email: 'E-mail',
emailAddress: 'E-mail cím',
@@ -236,6 +242,8 @@ export const huTranslations: DefaultTranslationsObject = {
filters: 'Szűrők',
filterWhere: 'Szűrő {{label}} ahol',
globals: 'Globálisok',
goBack: 'Vissza',
isEditing: 'szerkeszt',
language: 'Nyelv',
lastModified: 'Utoljára módosítva',
leaveAnyway: 'Távozás mindenképp',
@@ -291,6 +299,7 @@ export const huTranslations: DefaultTranslationsObject = {
success: 'Siker',
successfullyCreated: '{{label}} sikeresen létrehozva.',
successfullyDuplicated: '{{label}} sikeresen duplikálódott.',
takeOver: 'Átvétel',
thisLanguage: 'Magyar',
titleDeleted: '{{label}} "{{title}}" sikeresen törölve.',
true: 'Igaz',
@@ -306,6 +315,7 @@ export const huTranslations: DefaultTranslationsObject = {
username: 'Felhasználónév',
users: 'Felhasználók',
value: 'Érték',
viewReadOnly: 'Csak olvasható nézet',
welcome: 'Üdvözöljük',
},
operators: {

View File

@@ -181,6 +181,8 @@ export const itTranslations: DefaultTranslationsObject = {
addFilter: 'Aggiungi Filtro',
adminTheme: 'Tema Admin',
and: 'E',
anotherUserTakenOver:
'Un altro utente ha preso il controllo della modifica di questo documento.',
applyChanges: 'Applica modifiche',
ascending: 'Ascendente',
automatic: 'Automatico',
@@ -205,6 +207,8 @@ export const itTranslations: DefaultTranslationsObject = {
createNewLabel: 'Crea nuovo {{label}}',
creating: 'Crea nuovo',
creatingNewLabel: 'Creazione di un nuovo {{label}}',
currentlyEditing:
'sta attualmente modificando questo documento. Se prendi il controllo, verranno bloccati dal continuare a modificare e potrebbero anche perdere le modifiche non salvate.',
custom: 'Personalizzato',
dark: 'Scuro',
dashboard: 'Dashboard',
@@ -216,14 +220,17 @@ export const itTranslations: DefaultTranslationsObject = {
descending: 'Decrescente',
deselectAllRows: 'Deseleziona tutte le righe',
document: 'Documento',
documentLocked: 'Documento bloccato',
documents: 'Documenti',
duplicate: 'Duplica',
duplicateWithoutSaving: 'Duplica senza salvare le modifiche',
edit: 'Modificare',
editedSince: 'Modificato da',
editing: 'Modifica',
editingLabel_many: 'Modificare {{count}} {{label}}',
editingLabel_one: 'Modifica {{count}} {{label}}',
editingLabel_other: 'Modificare {{count}} {{label}}',
editingTakenOver: 'Modifica presa in carico',
editLabel: 'Modifica {{label}}',
email: 'Email',
emailAddress: 'Indirizzo Email',
@@ -236,6 +243,8 @@ export const itTranslations: DefaultTranslationsObject = {
filters: 'Filtri',
filterWhere: 'Filtra {{label}} se',
globals: 'Globali',
goBack: 'Torna indietro',
isEditing: 'sta modificando',
language: 'Lingua',
lastModified: 'Ultima modifica',
leaveAnyway: 'Esci comunque',
@@ -291,6 +300,7 @@ export const itTranslations: DefaultTranslationsObject = {
success: 'Successo',
successfullyCreated: '{{label}} creato con successo.',
successfullyDuplicated: '{{label}} duplicato con successo.',
takeOver: 'Prendi il controllo',
thisLanguage: 'Italiano',
titleDeleted: '{{label}} {{title}} eliminato con successo.',
true: 'Vero',
@@ -306,6 +316,7 @@ export const itTranslations: DefaultTranslationsObject = {
username: 'Nome utente',
users: 'Utenti',
value: 'Valore',
viewReadOnly: 'Visualizza solo lettura',
welcome: 'Benvenuto',
},
operators: {

View File

@@ -179,6 +179,7 @@ export const jaTranslations: DefaultTranslationsObject = {
addFilter: '絞り込みを追加',
adminTheme: '管理画面のテーマ',
and: 'かつ',
anotherUserTakenOver: '別のユーザーがこのドキュメントの編集を引き継ぎました。',
applyChanges: '変更を適用する',
ascending: '昇順',
automatic: '自動設定',
@@ -203,6 +204,8 @@ export const jaTranslations: DefaultTranslationsObject = {
createNewLabel: '{{label}} を新規作成',
creating: '作成中',
creatingNewLabel: '{{label}} を新規作成しています',
currentlyEditing:
'このドキュメントを編集中です。あなたが引き継ぐと、編集を続けることができなくなり、未保存の変更が失われる可能性があります。',
custom: 'カスタム',
dark: 'ダークモード',
dashboard: 'ダッシュボード',
@@ -214,14 +217,17 @@ export const jaTranslations: DefaultTranslationsObject = {
descending: '降順',
deselectAllRows: 'すべての行の選択を解除します',
document: 'ドキュメント',
documentLocked: 'ドキュメントがロックされました',
documents: 'ドキュメント',
duplicate: '複製',
duplicateWithoutSaving: '変更を保存せずに複製',
edit: '編集',
editedSince: 'から編集',
editing: '編集',
editingLabel_many: '{{count}}つの{{label}}を編集しています',
editingLabel_one: '{{count}}つの{{label}}を編集しています',
editingLabel_other: '{{count}}つの{{label}}を編集しています',
editingTakenOver: '編集が引き継がれました',
editLabel: '{{label}} を編集',
email: 'メールアドレス',
emailAddress: 'メールアドレス',
@@ -234,6 +240,8 @@ export const jaTranslations: DefaultTranslationsObject = {
filters: '絞り込み',
filterWhere: '{{label}} の絞り込み',
globals: 'グローバル',
goBack: '戻る',
isEditing: '編集中',
language: '言語',
lastModified: '最終更新',
leaveAnyway: 'すぐに画面を離れる',
@@ -289,6 +297,7 @@ export const jaTranslations: DefaultTranslationsObject = {
success: '成功',
successfullyCreated: '{{label}} が作成されました。',
successfullyDuplicated: '{{label}} が複製されました。',
takeOver: '引き継ぐ',
thisLanguage: 'Japanese',
titleDeleted: '{{label}} "{{title}}" が削除されました。',
true: '真実',
@@ -304,6 +313,7 @@ export const jaTranslations: DefaultTranslationsObject = {
username: 'ユーザーネーム',
users: 'ユーザー',
value: '値',
viewReadOnly: '読み取り専用で表示',
welcome: 'ようこそ',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const koTranslations: DefaultTranslationsObject = {
addFilter: '필터 추가',
adminTheme: '관리자 테마',
and: '및',
anotherUserTakenOver: '다른 사용자가 이 문서의 편집을 인수했습니다.',
applyChanges: '변경 사항 적용',
ascending: '오름차순',
automatic: '자동 설정',
@@ -202,6 +203,8 @@ export const koTranslations: DefaultTranslationsObject = {
createNewLabel: '새로운 {{label}} 생성',
creating: '생성 중',
creatingNewLabel: '{{label}} 생성 중',
currentlyEditing:
'현재 이 문서를 편집 중입니다. 당신이 인수하면, 편집을 계속할 수 없게 되고, 저장되지 않은 변경 사항이 손실될 수 있습니다.',
custom: '사용자 정의',
dark: '다크',
dashboard: '대시보드',
@@ -213,14 +216,17 @@ export const koTranslations: DefaultTranslationsObject = {
descending: '내림차순',
deselectAllRows: '모든 행 선택 해제',
document: '문서',
documentLocked: '문서가 잠겼습니다',
documents: '문서들',
duplicate: '복제',
duplicateWithoutSaving: '변경 사항 저장 없이 복제',
edit: '수정',
editedSince: '편집됨',
editing: '수정 중',
editingLabel_many: '{{count}}개의 {{label}} 수정 중',
editingLabel_one: '{{count}}개의 {{label}} 수정 중',
editingLabel_other: '{{count}}개의 {{label}} 수정 중',
editingTakenOver: '편집이 인수되었습니다',
editLabel: '{{label}} 수정',
email: '이메일',
emailAddress: '이메일 주소',
@@ -233,6 +239,8 @@ export const koTranslations: DefaultTranslationsObject = {
filters: '필터',
filterWhere: '{{label}} 필터링 조건',
globals: '글로벌',
goBack: '돌아가기',
isEditing: '편집 중',
language: '언어',
lastModified: '마지막 수정 일시',
leaveAnyway: '그래도 나가시겠습니까?',
@@ -288,6 +296,7 @@ export const koTranslations: DefaultTranslationsObject = {
success: '성공',
successfullyCreated: '{{label}}이(가) 생성되었습니다.',
successfullyDuplicated: '{{label}}이(가) 복제되었습니다.',
takeOver: '인수하기',
thisLanguage: '한국어',
titleDeleted: '{{label}} "{{title}}"을(를) 삭제했습니다.',
true: '참',
@@ -303,6 +312,7 @@ export const koTranslations: DefaultTranslationsObject = {
username: '사용자 이름',
users: '사용자',
value: '값',
viewReadOnly: '읽기 전용으로 보기',
welcome: '환영합니다',
},
operators: {

View File

@@ -180,6 +180,7 @@ export const myTranslations: DefaultTranslationsObject = {
addFilter: 'ဇကာထည့်ပါ။',
adminTheme: 'အက်ပ်ဒိုင်များစပ်စွာ',
and: 'နှင့်',
anotherUserTakenOver: 'တစ်ခြားအသုံးပြုသူသည်ဤစာရွက်စာတမ်းကိုပြင်ဆင်မှုကိုရယူလိုက်သည်။',
applyChanges: 'ပြောင်းလဲမှုများ အသုံးပြုပါ',
ascending: 'တက်နေသည်',
automatic: 'အော်တို',
@@ -205,6 +206,8 @@ export const myTranslations: DefaultTranslationsObject = {
createNewLabel: '{{label}} အသစ် ဖန်တီးမည်။',
creating: 'ဖန်တီးနေသည်။',
creatingNewLabel: '{{label}} အသစ် ဖန်တီးနေသည်။',
currentlyEditing:
'ဒီစာရွက်စာတမ်းကိုလက်ရှိပြင်ဆင်နေသည်။ သင်ဤတာဝန်ကိုယူပါက၊ သူတို့သည်ဆက်လက်ပြင်ဆင်ခွင့်မရအောင်ပိတ်ထားမည်ဖြစ်ပြီး၊ မသိမ်းဆည်းရသေးသောပြင်ဆင်မှုများကိုလည်းဆုံးရှုံးနိုင်သည်။',
custom: 'ထုတ်ကုန် စိတ်ကြိုက်',
dark: 'အမှောင်',
dashboard: 'ပင်မစာမျက်နှာ',
@@ -216,14 +219,17 @@ export const myTranslations: DefaultTranslationsObject = {
descending: 'ဆင်းသက်လာသည်။',
deselectAllRows: 'အားလုံးကို မရွေးနိုင်ပါ',
document: 'စာရွက်စာတမ်း',
documentLocked: 'စာရွက်စာတမ်းကိုပိတ်ထားသည်',
documents: 'စာရွက်စာတမ်းများ',
duplicate: 'ပုံတူပွားမည်။',
duplicateWithoutSaving: 'သေချာပါပြီ။',
edit: 'တည်းဖြတ်ပါ။',
editedSince: 'ကစပြီးတည်းဖြတ်ခဲ့သည်',
editing: 'ပြင်ဆင်နေသည်။',
editingLabel_many: 'တည်းဖြတ်ခြင်း {{count}} {{label}}',
editingLabel_one: 'တည်းဖြတ်ခြင်း {{count}} {{label}}',
editingLabel_other: 'တည်းဖြတ်ခြင်း {{count}} {{label}}',
editingTakenOver: 'တည်းဖြတ်ခြင်းကိုရယူခဲ့သည်',
editLabel: '{{label}} ပြင်ဆင်မည်။',
email: 'အီးမေးလ်',
emailAddress: 'အီးမေးလ် လိပ်စာ',
@@ -236,6 +242,8 @@ export const myTranslations: DefaultTranslationsObject = {
filters: 'စစ်ထုတ်မှုများ',
filterWhere: 'နေရာတွင် စစ်ထုတ်ပါ။',
globals: 'Globals',
goBack: 'နောက်သို့',
isEditing: 'ပြင်ဆင်နေသည်',
language: 'ဘာသာစကား',
lastModified: 'နောက်ဆုံးပြင်ဆင်ထားသည်။',
leaveAnyway: 'ဘာဖြစ်ဖြစ် ထွက်မည်။',
@@ -291,6 +299,7 @@ export const myTranslations: DefaultTranslationsObject = {
success: 'အောင်မြင်မှု',
successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။',
successfullyDuplicated: '{{label}} အောင်မြင်စွာ ပုံတူပွားခဲ့သည်။',
takeOver: 'တာဝန်ယူပါ',
thisLanguage: 'မြန်မာစာ',
titleDeleted: '{{label}} {{title}} အောင်မြင်စွာ ဖျက်သိမ်းခဲ့သည်။',
true: 'အမှန်',
@@ -307,6 +316,7 @@ export const myTranslations: DefaultTranslationsObject = {
username: 'Nama pengguna',
users: 'အသုံးပြုသူများ',
value: 'တန်ဖိုး',
viewReadOnly: 'ဖတ်ရှုရန်သာကြည့်ပါ',
welcome: 'ကြိုဆိုပါတယ်။',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const nbTranslations: DefaultTranslationsObject = {
addFilter: 'Legg til filter',
adminTheme: 'Admin-tema',
and: 'Og',
anotherUserTakenOver: 'En annen bruker har tatt over redigeringen av dette dokumentet.',
applyChanges: 'Bruk endringer',
ascending: 'Stigende',
automatic: 'Automatisk',
@@ -203,6 +204,8 @@ export const nbTranslations: DefaultTranslationsObject = {
createNewLabel: 'Opprett ny {{label}}',
creating: 'Oppretter',
creatingNewLabel: 'Oppretter ny {{label}}',
currentlyEditing:
'redigerer for øyeblikket dette dokumentet. Hvis du tar over, blir de blokkert fra å fortsette å redigere, og de kan også miste ulagrede endringer.',
custom: 'Tilpasset',
dark: 'Mørk',
dashboard: 'Kontrollpanel',
@@ -214,14 +217,17 @@ export const nbTranslations: DefaultTranslationsObject = {
descending: 'Synkende',
deselectAllRows: 'Fjern markeringen fra alle rader',
document: 'Dokument',
documentLocked: 'Dokument låst',
documents: 'Dokumenter',
duplicate: 'Dupliser',
duplicateWithoutSaving: 'Dupliser uten å lagre endringer',
edit: 'Redigere',
editedSince: 'Redigert siden',
editing: 'Redigerer',
editingLabel_many: 'Redigerer {{count}} {{label}}',
editingLabel_one: 'Redigerer {{count}} {{label}}',
editingLabel_other: 'Redigerer {{count}} {{label}}',
editingTakenOver: 'Redigering overtatt',
editLabel: 'Rediger {{label}}',
email: 'E-post',
emailAddress: 'E-postadresse',
@@ -234,6 +240,8 @@ export const nbTranslations: DefaultTranslationsObject = {
filters: 'Filter',
filterWhere: 'Filtrer {{label}} der',
globals: 'Globale variabler',
goBack: 'Gå tilbake',
isEditing: 'redigerer',
language: 'Språk',
lastModified: 'Sist endret',
leaveAnyway: 'Forlat likevel',
@@ -289,6 +297,7 @@ export const nbTranslations: DefaultTranslationsObject = {
success: 'Suksess',
successfullyCreated: '{{label}} ble opprettet.',
successfullyDuplicated: '{{label}} ble duplisert.',
takeOver: 'Ta over',
thisLanguage: 'Norsk',
titleDeleted: '{{label}} "{{title}}" ble slettet.',
true: 'Sann',
@@ -304,6 +313,7 @@ export const nbTranslations: DefaultTranslationsObject = {
username: 'Brukernavn',
users: 'Brukere',
value: 'Verdi',
viewReadOnly: 'Vis skrivebeskyttet',
welcome: 'Velkommen',
},
operators: {

View File

@@ -180,6 +180,7 @@ export const nlTranslations: DefaultTranslationsObject = {
addFilter: 'Filter toevoegen',
adminTheme: 'Adminthema',
and: 'En',
anotherUserTakenOver: 'Een andere gebruiker heeft de bewerking van dit document overgenomen.',
applyChanges: 'Breng wijzigingen aan',
ascending: 'Oplopend',
automatic: 'Automatisch',
@@ -205,6 +206,8 @@ export const nlTranslations: DefaultTranslationsObject = {
createNewLabel: 'Nieuw(e) {{label}} aanmaken',
creating: 'Aanmaken',
creatingNewLabel: 'Nieuw(e) {{label}} aanmaken',
currentlyEditing:
'is momenteel dit document aan het bewerken. Als je het overneemt, wordt voorkomen dat ze doorgaan met bewerken en kunnen ze ook niet-opgeslagen wijzigingen verliezen.',
custom: 'Aangepast',
dark: 'Donker',
dashboard: 'Dashboard',
@@ -216,14 +219,17 @@ export const nlTranslations: DefaultTranslationsObject = {
descending: 'Aflopend',
deselectAllRows: 'Deselecteer alle rijen',
document: 'Document',
documentLocked: 'Document vergrendeld',
documents: 'Documenten',
duplicate: 'Dupliceren',
duplicateWithoutSaving: 'Dupliceren zonder wijzigingen te bewaren',
edit: 'Bewerk',
editedSince: 'Bewerkt sinds',
editing: 'Bewerken',
editingLabel_many: 'Bewerken {{count}} {{label}}',
editingLabel_one: 'Bewerken {{count}} {{label}}',
editingLabel_other: 'Bewerken {{count}} {{label}}',
editingTakenOver: 'Bewerking overgenomen',
editLabel: 'Bewerk {{label}}',
email: 'E-mail',
emailAddress: 'E-maildres',
@@ -236,6 +242,8 @@ export const nlTranslations: DefaultTranslationsObject = {
filters: 'Filters',
filterWhere: 'Filter {{label}} waar',
globals: 'Globalen',
goBack: 'Ga terug',
isEditing: 'is aan het bewerken',
language: 'Taal',
lastModified: 'Laatst gewijzigd',
leaveAnyway: 'Toch weggaan',
@@ -291,6 +299,7 @@ export const nlTranslations: DefaultTranslationsObject = {
success: 'Succes',
successfullyCreated: '{{label}} succesvol aangemaakt.',
successfullyDuplicated: '{{label}} succesvol gedupliceerd.',
takeOver: 'Overnemen',
thisLanguage: 'Nederlands',
titleDeleted: '{{label}} "{{title}}" succesvol verwijderd.',
true: 'Waar',
@@ -306,6 +315,7 @@ export const nlTranslations: DefaultTranslationsObject = {
username: 'Gebruikersnaam',
users: 'Gebruikers',
value: 'Waarde',
viewReadOnly: 'Alleen-lezen weergave',
welcome: 'Welkom',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const plTranslations: DefaultTranslationsObject = {
addFilter: 'Dodaj filtr',
adminTheme: 'Motyw administratora',
and: 'i',
anotherUserTakenOver: 'Inny użytkownik przejął edycję tego dokumentu.',
applyChanges: 'Zastosuj zmiany',
ascending: 'Rosnąco',
automatic: 'Automatyczny',
@@ -203,6 +204,8 @@ export const plTranslations: DefaultTranslationsObject = {
createNewLabel: 'Stwórz nowy {{label}}',
creating: 'Tworzenie',
creatingNewLabel: 'Tworzenie nowego {{label}}',
currentlyEditing:
'obecnie edytuje ten dokument. Jeśli przejmiesz kontrolę, zostaną zablokowani przed dalszą edycją i mogą również utracić niezapisane zmiany.',
custom: 'Niestandardowy',
dark: 'Ciemny',
dashboard: 'Panel',
@@ -214,14 +217,17 @@ export const plTranslations: DefaultTranslationsObject = {
descending: 'Malejąco',
deselectAllRows: 'Odznacz wszystkie wiersze',
document: 'Dokument',
documentLocked: 'Dokument zablokowany',
documents: 'Dokumenty',
duplicate: 'Zduplikuj',
duplicateWithoutSaving: 'Zduplikuj bez zapisywania zmian',
edit: 'Edytuj',
editedSince: 'Edytowano od',
editing: 'Edycja',
editingLabel_many: 'Edytowanie {{count}} {{label}}',
editingLabel_one: 'Edytowanie {{count}} {{label}}',
editingLabel_other: 'Edytowanie {{count}} {{label}}',
editingTakenOver: 'Edycja przejęta',
editLabel: 'Edytuj {{label}}',
email: 'Email',
emailAddress: 'Adres email',
@@ -234,6 +240,8 @@ export const plTranslations: DefaultTranslationsObject = {
filters: 'Filtry',
filterWhere: 'Filtruj gdzie',
globals: 'Globalne',
goBack: 'Wróć',
isEditing: 'edytuje',
language: 'Język',
lastModified: 'Ostatnio zmodyfikowany',
leaveAnyway: 'Wyjdź mimo to',
@@ -289,6 +297,7 @@ export const plTranslations: DefaultTranslationsObject = {
success: 'Sukces',
successfullyCreated: 'Pomyślnie utworzono {{label}}.',
successfullyDuplicated: 'Pomyślnie zduplikowano {{label}}',
takeOver: 'Przejąć',
thisLanguage: 'Polski',
titleDeleted: 'Pomyślnie usunięto {{label}} {{title}}',
true: 'Prawda',
@@ -304,6 +313,7 @@ export const plTranslations: DefaultTranslationsObject = {
username: 'Nazwa użytkownika',
users: 'użytkownicy',
value: 'Wartość',
viewReadOnly: 'Widok tylko do odczytu',
welcome: 'Witaj',
},
operators: {

View File

@@ -179,6 +179,7 @@ export const ptTranslations: DefaultTranslationsObject = {
addFilter: 'Adicionar Filtro',
adminTheme: 'Tema do Admin',
and: 'E',
anotherUserTakenOver: 'Outro usuário assumiu a edição deste documento.',
applyChanges: 'Aplicar alterações',
ascending: 'Ascendente',
automatic: 'Automático',
@@ -204,6 +205,8 @@ export const ptTranslations: DefaultTranslationsObject = {
createNewLabel: 'Criar novo(a) {{label}}',
creating: 'Criando',
creatingNewLabel: 'Criando novo(a) {{label}}',
currentlyEditing:
'está editando este documento no momento. Se você assumir, eles serão impedidos de continuar editando e poderão perder alterações não salvas.',
custom: 'Personalizado',
dark: 'Escuro',
dashboard: 'Painel de Controle',
@@ -215,14 +218,17 @@ export const ptTranslations: DefaultTranslationsObject = {
descending: 'Decrescente',
deselectAllRows: 'Desmarcar todas as linhas',
document: 'Documento',
documentLocked: 'Documento bloqueado',
documents: 'Documentos',
duplicate: 'Duplicar',
duplicateWithoutSaving: 'Duplicar sem salvar alterações',
edit: 'Editar',
editedSince: 'Editado desde',
editing: 'Editando',
editingLabel_many: 'Editando {{count}} {{label}}',
editingLabel_one: 'Editando {{count}} {{label}}',
editingLabel_other: 'Editando {{count}} {{label}}',
editingTakenOver: 'Edição assumida',
editLabel: 'Editar {{label}}',
email: 'Email',
emailAddress: 'Endereço de Email',
@@ -235,6 +241,8 @@ export const ptTranslations: DefaultTranslationsObject = {
filters: 'Filtros',
filterWhere: 'Filtrar {{label}} em que',
globals: 'Globais',
goBack: 'Voltar',
isEditing: 'está editando',
language: 'Idioma',
lastModified: 'Última modificação',
leaveAnyway: 'Sair mesmo assim',
@@ -290,6 +298,7 @@ export const ptTranslations: DefaultTranslationsObject = {
success: 'Sucesso',
successfullyCreated: '{{label}} criado com sucesso.',
successfullyDuplicated: '{{label}} duplicado com sucesso.',
takeOver: 'Assumir',
thisLanguage: 'Português',
titleDeleted: '{{label}} {{title}} excluído com sucesso.',
true: 'Verdadeiro',
@@ -305,6 +314,7 @@ export const ptTranslations: DefaultTranslationsObject = {
username: 'Nome de usuário',
users: 'usuários',
value: 'Valor',
viewReadOnly: 'Visualizar somente leitura',
welcome: 'Boas vindas',
},
operators: {

View File

@@ -182,6 +182,7 @@ export const roTranslations: DefaultTranslationsObject = {
addFilter: 'Adaugă filtru',
adminTheme: 'Tema Admin',
and: 'Şi',
anotherUserTakenOver: 'Un alt utilizator a preluat editarea acestui document.',
applyChanges: 'Aplicați modificările',
ascending: 'Ascendant',
automatic: 'Automat',
@@ -207,6 +208,8 @@ export const roTranslations: DefaultTranslationsObject = {
createNewLabel: 'Creați un nou {{label}}',
creating: 'Creare',
creatingNewLabel: 'Crearea unui nou {{label}}',
currentlyEditing:
'editează în prezent acest document. Dacă preiei controlul, vor fi blocați să continue editarea și ar putea pierde modificările nesalvate.',
custom: 'Personalizat',
dark: 'Dark',
dashboard: 'Panoul de bord',
@@ -218,14 +221,17 @@ export const roTranslations: DefaultTranslationsObject = {
descending: 'Descendentă',
deselectAllRows: 'Deselectează toate rândurile',
document: 'Document',
documentLocked: 'Document blocat',
documents: 'Documente',
duplicate: 'Duplicați',
duplicateWithoutSaving: 'Duplicați fără salvarea modificărilor',
edit: 'Editează',
editedSince: 'Editat din',
editing: 'Editare',
editingLabel_many: 'Editare {{count}} {{label}}',
editingLabel_one: 'Editare {{count}} {{label}}',
editingLabel_other: 'Editare {{count}} {{label}}',
editingTakenOver: 'Editarea preluată',
editLabel: 'Editați {{label}}',
email: 'Email',
emailAddress: 'Adresa de email',
@@ -238,6 +244,8 @@ export const roTranslations: DefaultTranslationsObject = {
filters: 'Filtre',
filterWhere: 'Filtrează {{label}} unde',
globals: 'Globale',
goBack: 'Înapoi',
isEditing: 'editează',
language: 'Limba',
lastModified: 'Ultima modificare',
leaveAnyway: 'Pleacă oricum',
@@ -293,6 +301,7 @@ export const roTranslations: DefaultTranslationsObject = {
success: 'Succes',
successfullyCreated: '{{label}} creat(ă) cu succes.',
successfullyDuplicated: '{{label}} duplicat(ă) cu succes.',
takeOver: 'Preia controlul',
thisLanguage: 'Română',
titleDeleted: '{{label}} "{{title}}" șters cu succes.',
true: 'Adevărat',
@@ -308,6 +317,7 @@ export const roTranslations: DefaultTranslationsObject = {
username: 'Nume de utilizator',
users: 'Utilizatori',
value: 'Valoare',
viewReadOnly: 'Vizualizare doar pentru citire',
welcome: 'Bine ați venit',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const rsTranslations: DefaultTranslationsObject = {
addFilter: 'Додај филтер',
adminTheme: 'Администраторска тема',
and: 'И',
anotherUserTakenOver: 'Други корисник је преузео уређивање овог документа.',
applyChanges: 'Примени промене',
ascending: 'Узлазно',
automatic: 'Аутоматско',
@@ -202,6 +203,8 @@ export const rsTranslations: DefaultTranslationsObject = {
createNewLabel: 'Креирај ново {{label}}',
creating: 'Креира се',
creatingNewLabel: 'Креирање новог {{label}}',
currentlyEditing:
'тренутно уређује овај документ. Ако преузмете контролу, биће блокирани да наставе са уређивањем и могу изгубити несачуване измене.',
custom: 'Prilagođeno',
dark: 'Тамно',
dashboard: 'Контролни панел',
@@ -213,14 +216,17 @@ export const rsTranslations: DefaultTranslationsObject = {
descending: 'Опадајуће',
deselectAllRows: 'Деселектујте све редове',
document: 'Dokument',
documentLocked: 'Документ је закључан',
documents: 'Dokumenti',
duplicate: 'Дупликат',
duplicateWithoutSaving: 'Понови без чувања промена',
edit: 'Уреди',
editedSince: 'Измењено од',
editing: 'Уређивање',
editingLabel_many: 'Уређивање {{count}} {{label}}',
editingLabel_one: 'Уређивање {{count}} {{label}}',
editingLabel_other: 'Уређивање {{count}} {{label}}',
editingTakenOver: 'Уређивање преузето',
editLabel: 'Уреди {{label}}',
email: 'Е-пошта',
emailAddress: 'Адреса е-поште',
@@ -233,6 +239,8 @@ export const rsTranslations: DefaultTranslationsObject = {
filters: 'Филтери',
filterWhere: 'Филтер {{label}} где',
globals: 'Глобали',
goBack: 'Врати се',
isEditing: 'уређује',
language: 'Језик',
lastModified: 'Задња промена',
leaveAnyway: 'Свеједно напусти',
@@ -288,6 +296,7 @@ export const rsTranslations: DefaultTranslationsObject = {
success: 'Uspeh',
successfullyCreated: '{{label}} успешно креирано.',
successfullyDuplicated: '{{label}} успешно дуплицирано.',
takeOver: 'Превузети',
thisLanguage: 'Српски (ћирилица)',
titleDeleted: '{{label}} "{{title}}" успешно обрисано.',
true: 'Istinito',
@@ -303,6 +312,7 @@ export const rsTranslations: DefaultTranslationsObject = {
username: 'Korisničko ime',
users: 'Корисници',
value: 'Вредност',
viewReadOnly: 'Прегледај само за читање',
welcome: 'Добродошли',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
addFilter: 'Dodaj filter',
adminTheme: 'Administratorska tema',
and: 'I',
anotherUserTakenOver: 'Drugi korisnik je preuzeo uređivanje ovog dokumenta.',
applyChanges: 'Primeni promene',
ascending: 'Uzlazno',
automatic: 'Automatsko',
@@ -202,6 +203,8 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
createNewLabel: 'Kreiraj novo {{label}}',
creating: 'Kreira se',
creatingNewLabel: 'Kreiranje novog {{label}}',
currentlyEditing:
'trenutno uređuje ovaj dokument. Ako preuzmete kontrolu, biće blokirani da nastave sa uređivanjem i mogu izgubiti nesačuvane izmene.',
custom: 'Prilagođen',
dark: 'Tamno',
dashboard: 'Kontrolni panel',
@@ -213,14 +216,17 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
descending: 'Opadajuće',
deselectAllRows: 'Deselektujte sve redove',
document: 'Dokument',
documentLocked: 'Dokument je zaključan',
documents: 'Dokumenti',
duplicate: 'Duplikat',
duplicateWithoutSaving: 'Ponovi bez čuvanja promena',
edit: 'Uredi',
editedSince: 'Izmenjeno od',
editing: 'Uređivanje',
editingLabel_many: 'Uređivanje {{count}} {{label}}',
editingLabel_one: 'Uređivanje {{count}} {{label}}',
editingLabel_other: 'Uređivanje {{count}} {{label}}',
editingTakenOver: 'Uređivanje preuzeto',
editLabel: 'Uredi {{label}}',
email: 'E-pošta',
emailAddress: 'Аdresa e-pošte',
@@ -233,6 +239,8 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
filters: 'Filteri',
filterWhere: 'Filter {{label}} gde',
globals: 'Globali',
goBack: 'Vrati se',
isEditing: 'uređuje',
language: 'Jezik',
lastModified: 'Zadnja promena',
leaveAnyway: 'Svejedno napusti',
@@ -288,6 +296,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
success: 'Uspeh',
successfullyCreated: '{{label}} uspešno kreirano.',
successfullyDuplicated: '{{label}} uspešno duplicirano.',
takeOver: 'Preuzeti',
thisLanguage: 'Srpski (latinica)',
titleDeleted: '{{label}} "{{title}}" uspešno obrisano.',
true: 'Istinito',
@@ -303,6 +312,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
username: 'Korisničko ime',
users: 'Korisnici',
value: 'Vrednost',
viewReadOnly: 'Pregledaj samo za čitanje',
welcome: 'Dobrodošli',
},
operators: {

View File

@@ -180,6 +180,7 @@ export const ruTranslations: DefaultTranslationsObject = {
addFilter: 'Добавить фильтр',
adminTheme: 'Тема Панели',
and: 'А также',
anotherUserTakenOver: 'Другой пользователь взял на себя редактирование этого документа.',
applyChanges: 'Применить изменения',
ascending: 'Восходящий',
automatic: 'Автоматически',
@@ -205,6 +206,8 @@ export const ruTranslations: DefaultTranslationsObject = {
createNewLabel: 'Создать новый {{label}}',
creating: 'Создание',
creatingNewLabel: 'Создание нового {{label}}',
currentlyEditing:
'в настоящее время редактирует этот документ. Если вы возьмете на себя, они будут заблокированы от продолжения редактирования и могут потерять несохраненные изменения.',
custom: 'Обычай',
dark: 'Тёмная',
dashboard: 'Панель',
@@ -216,14 +219,17 @@ export const ruTranslations: DefaultTranslationsObject = {
descending: 'Уменьшение',
deselectAllRows: 'Снять выделение со всех строк',
document: 'Документ',
documentLocked: 'Документ заблокирован',
documents: 'Документы',
duplicate: 'Дублировать',
duplicateWithoutSaving: 'Дублирование без сохранения изменений',
edit: 'Редактировать',
editedSince: 'Отредактировано с',
editing: 'Редактирование',
editingLabel_many: 'Редактирование {{count}} {{label}}',
editingLabel_one: 'Редактирование {{count}} {{label}}',
editingLabel_other: 'Редактирование {{count}} {{label}}',
editingTakenOver: 'Редактирование взято под контроль',
editLabel: 'Редактировать {{label}}',
email: 'Email',
emailAddress: 'Email',
@@ -236,6 +242,8 @@ export const ruTranslations: DefaultTranslationsObject = {
filters: 'Фильтры',
filterWhere: 'Где фильтровать',
globals: 'Глобальные',
goBack: 'Назад',
isEditing: 'редактирует',
language: 'Язык',
lastModified: 'Последнее изменение',
leaveAnyway: 'Все равно уйти',
@@ -291,6 +299,7 @@ export const ruTranslations: DefaultTranslationsObject = {
success: 'Успех',
successfullyCreated: '{{label}} успешно создан.',
successfullyDuplicated: '{{label}} успешно продублирован.',
takeOver: 'Взять на себя',
thisLanguage: 'Русский',
titleDeleted: '{{label}} {{title}} успешно удалено.',
true: 'Правда',
@@ -307,6 +316,7 @@ export const ruTranslations: DefaultTranslationsObject = {
username: 'Имя пользователя',
users: 'пользователи',
value: 'Значение',
viewReadOnly: 'Просмотр только для чтения',
welcome: 'Добро пожаловать',
},
operators: {

View File

@@ -180,6 +180,7 @@ export const skTranslations: DefaultTranslationsObject = {
addFilter: 'Pridať filter',
adminTheme: 'Motív administračného rozhrania',
and: 'a',
anotherUserTakenOver: 'Iný používateľ prevzal úpravy tohto dokumentu.',
applyChanges: 'Použiť zmeny',
ascending: 'Vzostupne',
automatic: 'Automatický',
@@ -204,6 +205,8 @@ export const skTranslations: DefaultTranslationsObject = {
createNewLabel: 'Vytvoriť nový {{label}}',
creating: 'Vytváranie',
creatingNewLabel: 'Vytváranie nového {{label}}',
currentlyEditing:
'práve upravuje tento dokument. Ak prevezmete kontrolu, budú zablokovaní z pokračovania v úpravách a môžu tiež stratiť neuložené zmeny.',
custom: 'Vlastný',
dark: 'Tmavý',
dashboard: 'Nástenka',
@@ -215,14 +218,17 @@ export const skTranslations: DefaultTranslationsObject = {
descending: 'Zostupne',
deselectAllRows: 'Zrušiť výber všetkých riadkov',
document: 'Dokument',
documentLocked: 'Dokument je zamknutý',
documents: 'Dokumenty',
duplicate: 'Duplikovať',
duplicateWithoutSaving: 'Duplikovať bez uloženia zmien',
edit: 'Upraviť',
editedSince: 'Upravené od',
editing: 'Úpravy',
editingLabel_many: 'Úprava {{count}} {{label}}',
editingLabel_one: 'Úprava {{count}} {{label}}',
editingLabel_other: 'Úprava {{count}} {{label}}',
editingTakenOver: 'Úpravy prevzaté',
editLabel: 'Upraviť {{label}}',
email: 'E-mail',
emailAddress: 'E-mailová adresa',
@@ -235,6 +241,8 @@ export const skTranslations: DefaultTranslationsObject = {
filters: 'Filtry',
filterWhere: 'Filtrovat kde je {{label}}',
globals: 'Globalné',
goBack: 'Vrátiť sa',
isEditing: 'upravuje',
language: 'Jazyk',
lastModified: 'Naposledy zmenené',
leaveAnyway: 'Presto odísť',
@@ -290,6 +298,7 @@ export const skTranslations: DefaultTranslationsObject = {
success: 'Úspech',
successfullyCreated: '{{label}} úspešne vytvorené.',
successfullyDuplicated: '{{label}} úspešne duplikované.',
takeOver: 'Prevziať',
thisLanguage: 'Slovenčina',
titleDeleted: '{{label}} "{{title}}" úspešne zmazané.',
true: 'Pravda',
@@ -305,6 +314,7 @@ export const skTranslations: DefaultTranslationsObject = {
username: 'Používateľské meno',
users: 'Používatelia',
value: 'Hodnota',
viewReadOnly: 'Zobraziť iba na čítanie',
welcome: 'Vitajte',
},
operators: {

View File

@@ -178,6 +178,7 @@ export const svTranslations: DefaultTranslationsObject = {
addFilter: 'Lägg Till Filter',
adminTheme: 'Admin Tema',
and: 'Och',
anotherUserTakenOver: 'En annan användare har tagit över redigeringen av detta dokument.',
applyChanges: 'Verkställ ändringar',
ascending: 'Stigande',
automatic: 'Automatisk',
@@ -203,6 +204,8 @@ export const svTranslations: DefaultTranslationsObject = {
createNewLabel: 'Skapa ny {{label}}',
creating: 'Skapar',
creatingNewLabel: 'Skapar ny {{label}}',
currentlyEditing:
'redigerar för närvarande detta dokument. Om du tar över kommer de att blockeras från att fortsätta redigera och kan också förlora osparade ändringar.',
custom: 'Anpassad',
dark: 'Mörk',
dashboard: 'Manöverpanel',
@@ -214,14 +217,17 @@ export const svTranslations: DefaultTranslationsObject = {
descending: 'Fallande',
deselectAllRows: 'Avmarkera alla rader',
document: 'Dokument',
documentLocked: 'Dokument låst',
documents: 'Dokument',
duplicate: 'Duplicera',
duplicateWithoutSaving: 'Duplicera utan att spara ändringar',
edit: 'Redigera',
editedSince: 'Redigerad sedan',
editing: 'Redigerar',
editingLabel_many: 'Redigerar {{count}} {{label}}',
editingLabel_one: 'Redigerar {{count}} {{label}}',
editingLabel_other: 'Redigerar {{count}} {{label}}',
editingTakenOver: 'Redigering övertagen',
editLabel: 'Redigera {{label}}',
email: 'E-post',
emailAddress: 'E-postadress',
@@ -234,6 +240,8 @@ export const svTranslations: DefaultTranslationsObject = {
filters: 'Filter',
filterWhere: 'Filtrera {{label}} där',
globals: 'Globala',
goBack: 'Gå tillbaka',
isEditing: 'redigerar',
language: 'Språk',
lastModified: 'Senast Ändrad',
leaveAnyway: 'Lämna ändå',
@@ -289,6 +297,7 @@ export const svTranslations: DefaultTranslationsObject = {
success: 'Framgång',
successfullyCreated: '{{label}} skapades framgångsrikt.',
successfullyDuplicated: '{{label}} duplicerades framgångsrikt.',
takeOver: 'Ta över',
thisLanguage: 'Svenska',
titleDeleted: '{{label}} "{{title}}" togs bort framgångsrikt.',
true: 'Sann',
@@ -304,6 +313,7 @@ export const svTranslations: DefaultTranslationsObject = {
username: 'Användarnamn',
users: 'Användare',
value: 'Värde',
viewReadOnly: 'Visa endast läsning',
welcome: 'Välkommen',
},
operators: {

View File

@@ -175,6 +175,7 @@ export const thTranslations: DefaultTranslationsObject = {
addFilter: 'เพิ่มการกรอง',
adminTheme: 'ธีมผู้ดูแลระบบ',
and: 'และ',
anotherUserTakenOver: 'ผู้ใช้อื่นเข้าครอบครองการแก้ไขเอกสารนี้แล้ว',
applyChanges: 'ใช้การเปลี่ยนแปลง',
ascending: 'น้อยไปมาก',
automatic: 'อัตโนมัติ',
@@ -199,6 +200,8 @@ export const thTranslations: DefaultTranslationsObject = {
createNewLabel: 'สร้าง {{label}} ใหม่',
creating: 'กำลังสร้าง',
creatingNewLabel: 'กำลังสร้าง {{label}} ใหม่',
currentlyEditing:
'กำลังแก้ไขเอกสารนี้อยู่ในขณะนี้ หากคุณเข้าครอบครอง พวกเขาจะถูกบล็อกจากการแก้ไขต่อไป และอาจสูญเสียการเปลี่ยนแปลงที่ไม่ได้บันทึก',
custom: 'ที่ทำขึ้นเฉพาะ',
dark: 'มืด',
dashboard: 'แดชบอร์ด',
@@ -210,14 +213,17 @@ export const thTranslations: DefaultTranslationsObject = {
descending: 'มากไปน้อย',
deselectAllRows: 'ยกเลิกการเลือกทุกแถว',
document: 'เอกสาร',
documentLocked: 'เอกสารถูกล็อค',
documents: 'เอกสาร',
duplicate: 'สำเนา',
duplicateWithoutSaving: 'สำเนาโดยไม่บันทึกการแก้ไข',
edit: 'แก้ไข',
editedSince: 'แก้ไขตั้งแต่',
editing: 'แก้ไข',
editingLabel_many: 'กำลังแก้ไข {{count}} {{label}}',
editingLabel_one: 'กำลังแก้ไข {{count}} {{label}}',
editingLabel_other: 'กำลังแก้ไข {{count}} {{label}}',
editingTakenOver: 'การแก้ไขถูกครอบครอง',
editLabel: 'แก้ไข {{label}}',
email: 'อีเมล',
emailAddress: 'อีเมล',
@@ -230,6 +236,8 @@ export const thTranslations: DefaultTranslationsObject = {
filters: 'กรอง',
filterWhere: 'กรอง {{label}} เฉพาะ',
globals: 'Globals',
goBack: 'กลับไป',
isEditing: 'กำลังแก้ไข',
language: 'ภาษา',
lastModified: 'แก้ไขล่าสุดเมื่อ',
leaveAnyway: 'ออกจากหน้านี้',
@@ -285,6 +293,7 @@ export const thTranslations: DefaultTranslationsObject = {
success: 'ความสำเร็จ',
successfullyCreated: 'สร้าง {{label}} สำเร็จ',
successfullyDuplicated: 'สำเนา {{label}} สำเร็จ',
takeOver: 'เข้ายึด',
thisLanguage: 'ไทย',
titleDeleted: 'ลบ {{label}} "{{title}}" สำเร็จ',
true: 'จริง',
@@ -300,6 +309,7 @@ export const thTranslations: DefaultTranslationsObject = {
username: 'ชื่อผู้ใช้',
users: 'ผู้ใช้',
value: 'ค่า',
viewReadOnly: 'ดูในโหมดอ่านอย่างเดียว',
welcome: 'ยินดีต้อนรับ',
},
operators: {

View File

@@ -181,6 +181,7 @@ export const trTranslations: DefaultTranslationsObject = {
addFilter: 'Filtre ekle',
adminTheme: 'Admin arayüzü',
and: 've',
anotherUserTakenOver: 'Başka bir kullanıcı bu belgenin düzenlemesini devraldı.',
applyChanges: 'Değişiklikleri Uygula',
ascending: 'artan',
automatic: 'Otomatik',
@@ -206,6 +207,8 @@ export const trTranslations: DefaultTranslationsObject = {
createNewLabel: 'Yeni bir {{label}} oluştur',
creating: 'Oluşturuluyor',
creatingNewLabel: 'Yeni bir {{label}} oluşturuluyor',
currentlyEditing:
'şu anda bu belgeyi düzenliyor. Devralırsanız, düzenlemeye devam etmeleri engellenecek ve kaydedilmemiş değişiklikleri de kaybedebilirler.',
custom: 'Özel',
dark: 'Karanlık',
dashboard: 'Anasayfa',
@@ -217,14 +220,17 @@ export const trTranslations: DefaultTranslationsObject = {
descending: 'Azalan',
deselectAllRows: 'Tüm satırların seçimini kaldır',
document: 'Belge',
documentLocked: 'Belge kilitlendi',
documents: 'Belgeler',
duplicate: 'Çoğalt',
duplicateWithoutSaving: 'Ayarları kaydetmeden çoğalt',
edit: 'Düzenle',
editedSince: 'O tarihten itibaren düzenlendi',
editing: 'Düzenleniyor',
editingLabel_many: '{{count}} {{label}} düzenleniyor',
editingLabel_one: '{{count}} {{label}} düzenleniyor',
editingLabel_other: '{{count}} {{label}} düzenleniyor',
editingTakenOver: 'Düzenleme devralındı',
editLabel: '{{label}} düzenle',
email: 'E-posta',
emailAddress: 'E-posta adresi',
@@ -237,6 +243,8 @@ export const trTranslations: DefaultTranslationsObject = {
filters: 'Filtreler',
filterWhere: '{{label}} filtrele:',
globals: 'Globaller',
goBack: 'Geri dön',
isEditing: 'düzenliyor',
language: 'Dil',
lastModified: 'Son değiştirme',
leaveAnyway: 'Yine de ayrıl',
@@ -292,6 +300,7 @@ export const trTranslations: DefaultTranslationsObject = {
success: 'Başarı',
successfullyCreated: '{{label}} başarıyla oluşturuldu.',
successfullyDuplicated: '{{label}} başarıyla kopyalandı.',
takeOver: 'Devralmak',
thisLanguage: 'Türkçe',
titleDeleted: '{{label}} {{title}} başarıyla silindi.',
true: 'Doğru',
@@ -308,6 +317,7 @@ export const trTranslations: DefaultTranslationsObject = {
username: 'Kullanıcı Adı',
users: 'kullanıcı',
value: 'Değer',
viewReadOnly: 'Salt okunur olarak görüntüle',
welcome: 'Hoşgeldiniz',
},
operators: {

View File

@@ -179,6 +179,7 @@ export const ukTranslations: DefaultTranslationsObject = {
addFilter: 'Додати фільтр',
adminTheme: 'Тема адмін панелі',
and: 'і',
anotherUserTakenOver: 'Інший користувач взяв на себе редагування цього документа.',
applyChanges: 'Застосувати зміни',
ascending: 'В порядку зростання',
automatic: 'Автоматично',
@@ -203,6 +204,8 @@ export const ukTranslations: DefaultTranslationsObject = {
createNewLabel: 'Створити новий {{label}}',
creating: 'Створення',
creatingNewLabel: 'Створення нового {{label}}',
currentlyEditing:
'зараз редагує цей документ. Якщо ви переберете контроль, їм буде заблоковано продовження редагування, і вони також можуть втратити незбережені зміни.',
custom: 'Спеціальне замовлення',
dark: 'Темна',
dashboard: 'Головна',
@@ -214,14 +217,17 @@ export const ukTranslations: DefaultTranslationsObject = {
descending: 'В порядку спадання',
deselectAllRows: 'Скасувати вибір всіх рядків',
document: 'Документ',
documentLocked: 'Документ заблоковано',
documents: 'Документи',
duplicate: 'Дублювати',
duplicateWithoutSaving: 'Дублювання без збереження змін',
edit: 'Редагувати',
editedSince: 'Відредаговано з',
editing: 'Редагування',
editingLabel_many: 'Редагування {{count}} {{label}}',
editingLabel_one: 'Редагування {{count}} {{label}}',
editingLabel_other: 'Редагування {{count}} {{label}}',
editingTakenOver: 'Редагування взято на себе',
editLabel: 'Редагувати {{label}}',
email: 'Електронна пошта',
emailAddress: 'Адреса електронної пошти',
@@ -234,6 +240,8 @@ export const ukTranslations: DefaultTranslationsObject = {
filters: 'Фільтри',
filterWhere: 'Де фільтрувати {{label}}',
globals: 'Глобальні',
goBack: 'Повернутися',
isEditing: 'редагує',
language: 'Мова',
lastModified: 'Останні зміни',
leaveAnyway: 'Все одно вийти',
@@ -289,6 +297,7 @@ export const ukTranslations: DefaultTranslationsObject = {
success: 'Успіх',
successfullyCreated: '{{label}} успішно створено.',
successfullyDuplicated: '{{label}} успішно продубльовано.',
takeOver: 'Перейняти',
thisLanguage: 'Українська',
titleDeleted: '{{label}} "{{title}}" успішно видалено.',
true: 'Правда',
@@ -304,6 +313,7 @@ export const ukTranslations: DefaultTranslationsObject = {
username: "Ім'я користувача",
users: 'Користувачі',
value: 'Значення',
viewReadOnly: 'Перегляд тільки для читання',
welcome: 'Вітаю',
},
operators: {

View File

@@ -177,6 +177,7 @@ export const viTranslations: DefaultTranslationsObject = {
addFilter: 'Thêm bộ lọc',
adminTheme: 'Giao diện bảng điều khiển',
and: 'Và',
anotherUserTakenOver: 'Người dùng khác đã tiếp quản việc chỉnh sửa tài liệu này.',
applyChanges: 'Áp dụng Thay đổi',
ascending: 'Sắp xếp theo thứ tự tăng dần',
automatic: 'Tự động',
@@ -201,6 +202,8 @@ export const viTranslations: DefaultTranslationsObject = {
createNewLabel: 'Tạo mới {{label}}',
creating: 'Đang tạo',
creatingNewLabel: 'Đang tạo mới {{label}}',
currentlyEditing:
'hiện đang chỉnh sửa tài liệu này. Nếu bạn tiếp quản, họ sẽ bị chặn tiếp tục chỉnh sửa và cũng có thể mất các thay đổi chưa lưu.',
custom: 'Tùy chỉnh',
dark: 'Nền tối',
dashboard: 'Bảng điều khiển',
@@ -212,14 +215,17 @@ export const viTranslations: DefaultTranslationsObject = {
descending: 'Xếp theo thứ tự giảm dần',
deselectAllRows: 'Bỏ chọn tất cả các hàng',
document: 'Tài liệu',
documentLocked: 'Tài liệu bị khóa',
documents: 'Tài liệu',
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',
editedSince: 'Được chỉnh sửa từ',
editing: 'Đang chỉnh sửa',
editingLabel_many: 'Đang chỉnh sửa {{count}} {{label}}',
editingLabel_one: 'Đang chỉnh sửa {{count}} {{label}}',
editingLabel_other: 'Đang chỉnh sửa {{count}} {{label}}',
editingTakenOver: 'Chỉnh sửa đã được tiếp quản',
editLabel: 'Chỉnh sửa: {{label}}',
email: 'Email',
emailAddress: 'Địa chỉ Email',
@@ -232,6 +238,8 @@ export const viTranslations: DefaultTranslationsObject = {
filters: 'Bộ lọc',
filterWhere: 'Lọc {{label}} với điều kiện:',
globals: 'Toàn thể (globals)',
goBack: 'Quay lại',
isEditing: 'đang chỉnh sửa',
language: 'Ngôn ngữ',
lastModified: 'Chỉnh sửa lần cuối vào lúc',
leaveAnyway: 'Tiếp tục thoát',
@@ -287,6 +295,7 @@ export const viTranslations: DefaultTranslationsObject = {
success: 'Thành công',
successfullyCreated: '{{label}} đã được tạo thành công.',
successfullyDuplicated: '{{label}} đã được sao chép thành công.',
takeOver: 'Tiếp quản',
thisLanguage: 'Vietnamese (Tiếng Việt)',
titleDeleted: '{{label}} {{title}} đã được xóa thành công.',
true: 'Thật',
@@ -302,6 +311,7 @@ export const viTranslations: DefaultTranslationsObject = {
username: 'Tên đăng nhập',
users: 'Người dùng',
value: 'Giá trị',
viewReadOnly: 'Xem chỉ đọc',
welcome: 'Xin chào',
},
operators: {

View File

@@ -172,6 +172,7 @@ export const zhTranslations: DefaultTranslationsObject = {
addFilter: '添加过滤器',
adminTheme: '管理页面主题',
and: '和',
anotherUserTakenOver: '另一位用户接管了此文档的编辑。',
applyChanges: '应用更改',
ascending: '升序',
automatic: '自动',
@@ -196,6 +197,8 @@ export const zhTranslations: DefaultTranslationsObject = {
createNewLabel: '创建新的{{label}}',
creating: '创建中',
creatingNewLabel: '正在创建新的{{label}}',
currentlyEditing:
'当前正在编辑此文档。如果您接管,他们将无法继续编辑,并且可能会丢失未保存的更改。',
custom: '定制',
dark: '深色',
dashboard: '仪表板',
@@ -207,14 +210,17 @@ export const zhTranslations: DefaultTranslationsObject = {
descending: '降序',
deselectAllRows: '取消选择所有行',
document: '文件',
documentLocked: '文档已锁定',
documents: '文件',
duplicate: '重复',
duplicateWithoutSaving: '重复而不保存更改。',
edit: '编辑',
editedSince: '自...以来编辑',
editing: '编辑中',
editingLabel_many: '编辑 {{count}} {{label}}',
editingLabel_one: '编辑 {{count}} {{label}}',
editingLabel_other: '编辑 {{count}} {{label}}',
editingTakenOver: '编辑已被接管',
editLabel: '编辑{{label}}',
email: '电子邮件',
emailAddress: '电子邮件地址',
@@ -227,6 +233,8 @@ export const zhTranslations: DefaultTranslationsObject = {
filters: '过滤器',
filterWhere: '过滤{{label}}',
globals: '全局',
goBack: '返回',
isEditing: '正在编辑',
language: '语言',
lastModified: '最后修改',
leaveAnyway: '无论如何都要离开',
@@ -281,6 +289,7 @@ export const zhTranslations: DefaultTranslationsObject = {
success: '成功',
successfullyCreated: '成功创建{{label}}',
successfullyDuplicated: '成功复制{{label}}',
takeOver: '接管',
thisLanguage: '中文 (简体)',
titleDeleted: '{{label}} "{{title}}"已被成功删除。',
true: '真实',
@@ -296,6 +305,7 @@ export const zhTranslations: DefaultTranslationsObject = {
username: '用户名',
users: '用户',
value: '值',
viewReadOnly: '只读查看',
welcome: '欢迎',
},
operators: {

View File

@@ -172,6 +172,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
addFilter: '新增過濾器',
adminTheme: '管理頁面主題',
and: '和',
anotherUserTakenOver: '另一位使用者接管了此文件的編輯。',
applyChanges: '套用更改',
ascending: '升冪',
automatic: '自動',
@@ -196,6 +197,8 @@ export const zhTwTranslations: DefaultTranslationsObject = {
createNewLabel: '建立新的{{label}}',
creating: '建立中',
creatingNewLabel: '正在建立新的{{label}}',
currentlyEditing:
'目前正在編輯此文件。如果您接管,他們將無法繼續編輯,並且可能會丟失未保存的更改。',
custom: '自訂',
dark: '深色',
dashboard: '控制面板',
@@ -207,14 +210,17 @@ export const zhTwTranslations: DefaultTranslationsObject = {
descending: '降冪',
deselectAllRows: '取消選擇全部',
document: '文件',
documentLocked: '文件已鎖定',
documents: '文件',
duplicate: '複製',
duplicateWithoutSaving: '複製而不儲存變更。',
edit: '編輯',
editedSince: '自...以來編輯',
editing: '編輯中',
editingLabel_many: '編輯 {{count}} 個 {{label}}',
editingLabel_one: '編輯 {{count}} 個 {{label}}',
editingLabel_other: '編輯 {{count}} 個 {{label}}',
editingTakenOver: '編輯已被接管',
editLabel: '編輯{{label}}',
email: '電子郵件',
emailAddress: '電子郵件地址',
@@ -227,6 +233,8 @@ export const zhTwTranslations: DefaultTranslationsObject = {
filters: '過濾器',
filterWhere: '過濾{{label}}',
globals: '全域',
goBack: '返回',
isEditing: '正在編輯',
language: '語言',
lastModified: '最後修改',
leaveAnyway: '無論如何都要離開',
@@ -281,6 +289,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
success: '成功',
successfullyCreated: '成功建立{{label}}',
successfullyDuplicated: '成功複製{{label}}',
takeOver: '接管',
thisLanguage: '中文 (繁體)',
titleDeleted: '{{label}} "{{title}}"已被成功刪除。',
true: '真實',
@@ -296,6 +305,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
username: '使用者名稱',
users: '使用者',
value: '值',
viewReadOnly: '僅檢視',
welcome: '歡迎',
},
operators: {

View File

@@ -114,7 +114,7 @@ export function EditForm({ submitted }: EditFormProps) {
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
async ({ formState: prevFormState }) => {
const docPreferences = await getDocPreferences()
const newFormState = await getFormState({
const { state: newFormState } = await getFormState({
apiRoute,
body: {
collectionSlug,

View File

@@ -152,7 +152,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
}
try {
const formStateWithoutFiles = await getFormState({
const { state: formStateWithoutFiles } = await getFormState({
apiRoute: config.routes.api,
body: {
collectionSlug,

View File

@@ -53,6 +53,14 @@
}
}
&__locked-controls.locked {
position: unset;
.tooltip {
top: calc(var(--base) * -0.5);
}
}
&__list-item {
display: flex;
align-items: center;

View File

@@ -2,6 +2,7 @@
import type {
ClientCollectionConfig,
ClientGlobalConfig,
ClientUser,
CollectionPermission,
GlobalPermission,
SanitizedCollectionConfig,
@@ -18,9 +19,11 @@ import { useTranslation } from '../../providers/Translation/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { formatDate } from '../../utilities/formatDate.js'
import { Autosave } from '../Autosave/index.js'
import { Button } from '../Button/index.js'
import { DeleteDocument } from '../DeleteDocument/index.js'
import { DuplicateDocument } from '../DuplicateDocument/index.js'
import { Gutter } from '../Gutter/index.js'
import { Locked } from '../Locked/index.js'
import { Popup, PopupList } from '../Popup/index.js'
import { PreviewButton } from '../PreviewButton/index.js'
import { PublishButton } from '../PublishButton/index.js'
@@ -46,10 +49,13 @@ export const DocumentControls: React.FC<{
/* Only available if `redirectAfterDuplicate` is `false` */
readonly onDuplicate?: DocumentInfoContext['onDuplicate']
readonly onSave?: DocumentInfoContext['onSave']
readonly onTakeOver?: () => void
readonly permissions: CollectionPermission | GlobalPermission | null
readonly readOnlyForIncomingUser?: boolean
readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean
readonly slug: SanitizedCollectionConfig['slug']
readonly user?: ClientUser
}> = (props) => {
const {
id,
@@ -63,12 +69,15 @@ export const DocumentControls: React.FC<{
onDelete,
onDrawerCreate,
onDuplicate,
onTakeOver,
permissions,
readOnlyForIncomingUser,
redirectAfterDelete,
redirectAfterDuplicate,
user,
} = props
const { i18n } = useTranslation()
const { i18n, t } = useTranslation()
const editDepth = useEditDepth()
@@ -125,6 +134,9 @@ export const DocumentControls: React.FC<{
</p>
</li>
)}
{user && readOnlyForIncomingUser && (
<Locked className={`${baseClass}__locked-controls`} user={user} />
)}
{(collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts) && (
<Fragment>
{(globalConfig || (collectionConfig && isEditing)) && (
@@ -219,6 +231,17 @@ export const DocumentControls: React.FC<{
)}
</React.Fragment>
)}
{user && readOnlyForIncomingUser && (
<Button
buttonStyle="secondary"
id="take-over"
onClick={() => void onTakeOver()}
size="medium"
type="button"
>
{t('general:takeOver')}
</Button>
)}
</div>
{showDotMenu && (
<Popup

View File

@@ -126,7 +126,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
React.useEffect(() => {
if (!hasInitializedState.current) {
const getInitialState = async () => {
const result = await getFormState({
const { state: result } = await getFormState({
apiRoute,
body: {
collectionSlug: slug,
@@ -146,8 +146,8 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
}, [apiRoute, hasInitializedState, serverURL, slug])
const onChange: FormProps['onChange'][0] = useCallback(
({ formState: prevFormState }) =>
getFormState({
async ({ formState: prevFormState }) => {
const { state } = await getFormState({
apiRoute,
body: {
collectionSlug: slug,
@@ -156,7 +156,10 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
schemaPath: slug,
},
serverURL,
}),
})
return state
},
[serverURL, apiRoute, slug],
)

View File

@@ -0,0 +1,14 @@
@import '../../scss/styles.scss';
.locked {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
pointer-events: all;
&__tooltip {
left: 0;
transform: translate3d(-0%, calc(var(--caret-size) * -1), 0);
}
}

View File

@@ -0,0 +1,38 @@
'use client'
import type { ClientUser } from 'payload'
import React, { useState } from 'react'
import { useTableCell } from '../../elements/Table/TableCellProvider/index.js'
import { LockIcon } from '../../icons/Lock/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { Tooltip } from '../Tooltip/index.js'
import './index.scss'
const baseClass = 'locked'
export const Locked: React.FC<{ className?: string; user: ClientUser }> = ({ className, user }) => {
const { rowData } = useTableCell()
const [hovered, setHovered] = useState(false)
const { t } = useTranslation()
const userToUse = user ? (user?.email ?? user?.id) : rowData?.id
return (
<div
className={[baseClass, className].filter(Boolean).join(' ')}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
role="button"
tabIndex={0}
>
<Tooltip
alignCaret="left"
className={`${baseClass}__tooltip`}
position="top"
show={hovered}
>{`${userToUse} ${t('general:isEditing')}`}</Tooltip>
<LockIcon />
</div>
)
}

View File

@@ -4,6 +4,7 @@ import React from 'react'
import { useTableCell } from '../../elements/Table/TableCellProvider/index.js'
import { CheckboxInput } from '../../fields/Checkbox/Input.js'
import { useSelection } from '../../providers/Selection/index.js'
import { Locked } from '../Locked/index.js'
import './index.scss'
const baseClass = 'select-row'
@@ -11,6 +12,13 @@ const baseClass = 'select-row'
export const SelectRow: React.FC = () => {
const { selected, setSelection } = useSelection()
const { rowData } = useTableCell()
const { isLocked, userEditing } = rowData || {}
const documentIsLocked = isLocked && userEditing
if (documentIsLocked) {
return <Locked user={userEditing} />
}
return (
<CheckboxInput

View File

@@ -53,7 +53,7 @@
}
&--position-top {
top: calc(var(--base) * -0.6 - 13px);
top: calc(var(--base) * -1.25);
transform: translate3d(-50%, calc(var(--caret-size) * -1), 0);
&::after {
@@ -63,7 +63,7 @@
}
&--position-bottom {
bottom: calc(var(--base) * -0.6 - 13px);
bottom: calc(var(--base) * -1.25);
transform: translate3d(-50%, var(--caret-size), 0);
&::after {

View File

@@ -10,6 +10,7 @@ export type Props = {
children: React.ReactNode
className?: string
delay?: number
position?: 'bottom' | 'top'
show?: boolean
/**
* If the tooltip position should not change depending on if the toolbar is outside the boundingRef. @default false
@@ -24,6 +25,7 @@ export const Tooltip: React.FC<Props> = (props) => {
children,
className,
delay = 350,
position: positionFromProps,
show: showFromProps = true,
staticPositioning = false,
} = props
@@ -89,7 +91,7 @@ export const Tooltip: React.FC<Props> = (props) => {
className,
show && 'tooltip--show',
`tooltip--caret-${alignCaret}`,
`tooltip--position-${position}`,
`tooltip--position-${positionFromProps || position}`,
]
.filter(Boolean)
.join(' ')}

View File

@@ -52,6 +52,7 @@ export { GenerateConfirmation } from '../../elements/GenerateConfirmation/index.
export { Gutter } from '../../elements/Gutter/index.js'
export { Hamburger } from '../../elements/Hamburger/index.js'
export { HydrateAuthProvider } from '../../elements/HydrateAuthProvider/index.js'
export { Locked } from '../../elements/Locked/index.js'
export { ListControls } from '../../elements/ListControls/index.js'
export { useListDrawer } from '../../elements/ListDrawer/index.js'
export { ListSelection } from '../../elements/ListSelection/index.js'

View File

@@ -451,7 +451,7 @@ export const Form: React.FC<FormProps> = (props) => {
const reset = useCallback(
async (data: unknown) => {
const newState = await getFormState({
const { state: newState } = await getFormState({
apiRoute,
body: {
id,
@@ -482,7 +482,7 @@ export const Form: React.FC<FormProps> = (props) => {
const getFieldStateBySchemaPath = useCallback(
async ({ data, schemaPath }) => {
const fieldSchema = await getFormState({
const { state: fieldSchema } = await getFormState({
apiRoute,
body: {
collectionSlug,

View File

@@ -53,11 +53,14 @@ export const RenderField: React.FC<Props> = ({
return null
}
// Combine readOnlyFromContext with the readOnly prop passed down from RenderFields
const isReadOnly = fieldComponentProps.readOnly ?? readOnlyFromContext
// `admin.readOnly` displays the value but prevents the field from being edited
fieldComponentProps.readOnly = fieldComponentProps?.field?.admin?.readOnly
// if parent field is `readOnly: true`, but this field is `readOnly: false`, the field should still be editable
if (readOnlyFromContext && fieldComponentProps.readOnly !== false) {
if (isReadOnly && fieldComponentProps.readOnly !== false) {
fieldComponentProps.readOnly = true
}

View File

@@ -13,8 +13,17 @@ const baseClass = 'render-fields'
export { Props }
export const RenderFields: React.FC<Props> = (props) => {
const { className, fields, forceRender, indexPath, margins, path, permissions, schemaPath } =
props
const {
className,
fields,
forceRender,
indexPath,
margins,
path,
permissions,
readOnly,
schemaPath,
} = props
const { i18n } = useTranslation()
const [hasRendered, setHasRendered] = React.useState(Boolean(forceRender))
@@ -65,7 +74,7 @@ export const RenderFields: React.FC<Props> = (props) => {
return (
<RenderField
fieldComponentProps={{ field, forceRender: forceRenderChildren }}
fieldComponentProps={{ field, forceRender: forceRenderChildren, readOnly }}
indexPath={indexPath !== undefined ? `${indexPath}.${fieldIndex}` : `${fieldIndex}`}
key={fieldIndex}
name={name}

View File

@@ -29,7 +29,9 @@ export type BuildFormStateArgs = {
id?: number | string
locale?: string
operation?: 'create' | 'update'
returnLockStatus?: boolean
schemaPath: string
updateLastEdited?: boolean
}
export const buildStateFromSchema = async (args: Args): Promise<FormState> => {

View File

@@ -0,0 +1,8 @@
@import '../../scss/styles';
.icon--lock {
.stroke {
stroke: currentColor;
stroke-width: $style-stroke-width;
}
}

View File

@@ -0,0 +1,29 @@
import React from 'react'
import './index.scss'
export const LockIcon: React.FC<{ className?: string }> = ({ className }) => (
<svg
className={['icon icon--lock', className].filter(Boolean).join(' ')}
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
className="stroke"
d="M7.5 9.5V7.5C7.5 6.83696 7.76339 6.20107 8.23223 5.73223C8.70107 5.26339 9.33696 5 10 5C10.663 5 11.2989 5.26339 11.7678 5.73223C12.2366 6.20107 12.5 6.83696 12.5 7.5V9.5"
strokeLinecap="round"
strokeLinejoin="round"
strokeOpacity="1"
/>
<path
className="stroke"
d="M13.5 9.5H6.5C5.94772 9.5 5.5 9.94772 5.5 10.5V14C5.5 14.5523 5.94772 15 6.5 15H13.5C14.0523 15 14.5 14.5523 14.5 14V10.5C14.5 9.94772 14.0523 9.5 13.5 9.5Z"
stopOpacity="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)

View File

@@ -1,5 +1,6 @@
'use client'
import type {
ClientUser,
Data,
DocumentPermissions,
DocumentPreferences,
@@ -18,6 +19,7 @@ import React, { createContext, useCallback, useContext, useEffect, useRef, useSt
import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
import { requests } from '../../utilities/api.js'
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
import { getFormState } from '../../utilities/getFormState.js'
import { hasSavePermission as getHasSavePermission } from '../../utilities/hasSavePermission.js'
@@ -67,6 +69,10 @@ const DocumentInfo: React.FC<
const globalConfig = globals.find((g) => g.slug === globalSlug)
const docConfig = collectionConfig || globalConfig
const lockDocumentsProp = docConfig?.lockDocuments !== undefined ? docConfig?.lockDocuments : true
const isLockingEnabled = lockDocumentsProp !== false
const { i18n } = useTranslation()
const { uploadEdits } = useUploadEdits()
@@ -97,6 +103,10 @@ const DocumentInfo: React.FC<
const [hasPublishPermission, setHasPublishPermission] = useState<boolean>(
hasPublishPermissionFromProps,
)
const [documentIsLocked, setDocumentIsLocked] = useState<boolean | undefined>(false)
const [currentEditor, setCurrentEditor] = useState<ClientUser | null>(null)
const isInitializing = initialState === undefined || data === undefined
const [unpublishedVersions, setUnpublishedVersions] =
useState<PaginatedDocs<TypeWithVersion<any>>>(null)
@@ -106,8 +116,6 @@ const DocumentInfo: React.FC<
const { code: locale } = useLocale()
const prevLocale = useRef(locale)
const hasInitializedDocPermissions = useRef(false)
// Separate locale cache used for handling permissions
const prevLocalePermissions = useRef(locale)
const versionsConfig = docConfig?.versions
@@ -135,6 +143,109 @@ const DocumentInfo: React.FC<
const operation = isEditing ? 'update' : 'create'
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions?.permission)
const unlockDocument = useCallback(
async (docId: number | string, slug: string) => {
try {
const isGlobal = slug === globalSlug
const query = isGlobal
? `where[globalSlug][equals]=${slug}`
: `where[document.value][equals]=${docId}&where[document.relationTo][equals]=${slug}`
const request = await requests.get(`${serverURL}${api}/payload-locked-documents?${query}`)
const { docs } = await request.json()
if (docs.length > 0) {
const lockId = docs[0].id
await requests.delete(`${serverURL}${api}/payload-locked-documents/${lockId}`, {
headers: {
'Content-Type': 'application/json',
},
})
setDocumentIsLocked(false)
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to unlock the document', error)
}
},
[serverURL, api, globalSlug],
)
const updateDocumentEditor = useCallback(
async (docId: number | string, slug: string, user: ClientUser) => {
try {
const isGlobal = slug === globalSlug
const query = isGlobal
? `where[globalSlug][equals]=${slug}`
: `where[document.value][equals]=${docId}&where[document.relationTo][equals]=${slug}`
// Check if the document is already locked
const request = await requests.get(`${serverURL}${api}/payload-locked-documents?${query}`)
const { docs } = await request.json()
if (docs.length > 0) {
const lockId = docs[0].id
// Send a patch request to update the _lastEdited info
await requests.patch(`${serverURL}${api}/payload-locked-documents/${lockId}`, {
body: JSON.stringify({
_lastEdited: {
editedAt: new Date(),
user: { relationTo: user?.collection, value: user?.id },
},
}),
headers: {
'Content-Type': 'application/json',
},
})
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to update the document editor', error)
}
},
[serverURL, api, globalSlug],
)
useEffect(() => {
if (!isLockingEnabled || (!id && !globalSlug)) {
return
}
const fetchDocumentLockState = async () => {
if (id || globalSlug) {
try {
const slug = collectionSlug ?? globalSlug
const isGlobal = slug === globalSlug
const query = isGlobal
? `where[globalSlug][equals]=${slug}`
: `where[document.value][equals]=${id}&where[document.relationTo][equals]=${slug}`
const request = await requests.get(`${serverURL}${api}/payload-locked-documents?${query}`)
const { docs } = await request.json()
if (docs.length > 0) {
const newEditor = docs[0]._lastEdited?.user?.value
if (newEditor && newEditor.id !== currentEditor?.id) {
setCurrentEditor(newEditor)
setDocumentIsLocked(true)
}
} else {
setDocumentIsLocked(false)
}
} catch (error) {
// swallow error
}
}
}
void fetchDocumentLockState()
}, [id, serverURL, api, collectionSlug, globalSlug, currentEditor, isLockingEnabled])
const getVersions = useCallback(async () => {
let versionFetchURL
let publishedFetchURL
@@ -389,7 +500,7 @@ const DocumentInfo: React.FC<
const newData = collectionSlug ? json.doc : json.result
const newState = await getFormState({
const { state: newState } = await getFormState({
apiRoute: api,
body: {
id,
@@ -406,6 +517,7 @@ const DocumentInfo: React.FC<
setInitialState(newState)
setData(newData)
await getDocPermissions(newData)
},
[
@@ -440,7 +552,7 @@ const DocumentInfo: React.FC<
setIsLoading(true)
try {
const result = await getFormState({
const { state: result } = await getFormState({
apiRoute: api,
body: {
id,
@@ -564,8 +676,10 @@ const DocumentInfo: React.FC<
const value: DocumentInfoContext = {
...props,
action,
currentEditor,
docConfig,
docPermissions,
documentIsLocked,
getDocPermissions,
getDocPreferences,
getVersions,
@@ -578,10 +692,14 @@ const DocumentInfo: React.FC<
onSave,
preferencesKey,
publishedDoc,
setCurrentEditor,
setDocFieldPreferences,
setDocumentIsLocked,
setDocumentTitle,
title: documentTitle,
unlockDocument,
unpublishedVersions,
updateDocumentEditor,
versions,
}

View File

@@ -1,6 +1,7 @@
import type {
ClientCollectionConfig,
ClientGlobalConfig,
ClientUser,
Data,
DocumentPermissions,
DocumentPreferences,
@@ -56,7 +57,9 @@ export type DocumentInfoProps = {
}
export type DocumentInfoContext = {
currentEditor?: ClientUser
docConfig?: ClientCollectionConfig | ClientGlobalConfig
documentIsLocked?: boolean
getDocPermissions: (data?: Data) => Promise<void>
getDocPreferences: () => Promise<DocumentPreferences>
getVersions: () => Promise<void>
@@ -66,13 +69,17 @@ export type DocumentInfoContext = {
isLoading: boolean
preferencesKey?: string
publishedDoc?: { _status?: string } & TypeWithID & TypeWithTimestamps
setCurrentEditor?: React.Dispatch<React.SetStateAction<ClientUser>>
setDocFieldPreferences: (
field: string,
fieldPreferences: { [key: string]: unknown } & Partial<InsideFieldsPreferences>,
) => void
setDocumentIsLocked?: React.Dispatch<React.SetStateAction<boolean>>
setDocumentTitle: (title: string) => void
title: string
unlockDocument: (docId: number | string, slug: string) => Promise<void>
unpublishedVersions?: PaginatedDocs<TypeWithVersion<any>>
updateDocumentEditor: (docId: number | string, slug: string, user: ClientUser) => Promise<void>
versions?: PaginatedDocs<TypeWithVersion<any>>
versionsCount?: PaginatedDocs<TypeWithVersion<any>>
} & DocumentInfoProps

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