feat: duplicate doc moved from frontend to backend concern (#5342)

BREAKING CHANGE: collection.admin.hooks.beforeDuplicate removed and instead should be handled using field beforeDuplicate hooks which take the full field hook arguments.

* feat: duplicate doc moved from frontend to backend concern

* feat: default beforeDuplicate hook functions on unique fields

* docs: beforeDuplicate field hook

* test: duplicate doc local api

* chore: fix build errors

* chore: add access.create call to duplicate operation

* chore: perfectionist reorder imports
This commit is contained in:
Dan Ribbens
2024-03-19 11:25:19 -04:00
committed by GitHub
parent b259bc60a2
commit ed01ee1e2d
90 changed files with 748 additions and 269 deletions

View File

@@ -167,53 +167,6 @@ those three fields plus the ID field.
so your admin queries can remain performant. so your admin queries can remain performant.
</Banner> </Banner>
### Admin Hooks
In addition to collection hooks themselves, Payload provides for admin UI-specific hooks that you can leverage.
**`beforeDuplicate`**
The `beforeDuplicate` hook is an async function that accepts an object containing the data to duplicate, as well as the
locale of the doc to duplicate. Within this hook, you can modify the data to be duplicated, which is useful in cases
where you have unique fields that need to be incremented or similar, as well as if you want to automatically modify a
document's `title`.
Example:
```ts
import { BeforeDuplicate, CollectionConfig } from 'payload/types'
// Your auto-generated Page type
import { Page } from '../payload-types.ts'
const beforeDuplicate: BeforeDuplicate<Page> = ({ data }) => {
return {
...data,
title: `${data.title} Copy`,
uniqueField: data.uniqueField ? `${data.uniqueField}-copy` : '',
}
}
export const Page: CollectionConfig = {
slug: 'pages',
admin: {
hooks: {
beforeDuplicate,
},
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'uniqueField',
type: 'text',
unique: true,
},
],
}
```
### TypeScript ### TypeScript
You can import collection types as follows: You can import collection types as follows:

View File

@@ -21,6 +21,7 @@ functionalities to be easily reusable across your projects.
- [beforeValidate](#beforevalidate) - [beforeValidate](#beforevalidate)
- [beforeChange](#beforechange) - [beforeChange](#beforechange)
- beforeDuplicate(#beforeduplicate)
- [afterChange](#afterchange) - [afterChange](#afterchange)
- [afterRead](#afterread) - [afterRead](#afterread)
@@ -38,6 +39,7 @@ const ExampleField: Field = {
hooks: { hooks: {
beforeValidate: [(args) => {...}], beforeValidate: [(args) => {...}],
beforeChange: [(args) => {...}], beforeChange: [(args) => {...}],
beforeDuplicate: [(args) => {...}],
afterChange: [(args) => {...}], afterChange: [(args) => {...}],
afterRead: [(args) => {...}], afterRead: [(args) => {...}],
} }
@@ -217,6 +219,27 @@ Here, the `afterRead` hook for the `dateField` is used to format the date into a
using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more
user-friendly. user-friendly.
### beforeDuplicate
The `beforeDuplicate` field hook is only called when duplicating a document. It may be used when documents having the
exact same properties may cause issue. This gives you a way to avoid duplicate names on `unique`, `required` fields or
to unset values by returning `null`. This is called immediately after `defaultValue` and before validation occurs.
```ts
import { Field } from 'payload/types'
const numberField: Field = {
name: 'number',
type: 'number',
hooks: {
// increment existing value by 1
beforeDuplicate: [({ value }) => {
return (value ?? 0) + 1
}],
}
}
```
## TypeScript ## TypeScript
Payload exports a type for field hooks which can be accessed and used as follows: Payload exports a type for field hooks which can be accessed and used as follows:

View File

@@ -0,0 +1,45 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequest } from 'payload/types'
import type { Collection } from 'payload/types'
import { duplicateOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'
import type { Context } from '../types.js'
export type Resolver<T> = (
_: unknown,
args: {
draft: boolean
fallbackLocale?: string
id: string
locale?: string
},
context: {
req: PayloadRequest
},
) => Promise<T>
export default function duplicateResolver<T extends keyof GeneratedTypes['collections']>(
collection: Collection,
): Resolver<GeneratedTypes['collections'][T]> {
return async function resolver(_, args, context: Context) {
const { req } = context
const locale = req.locale
const fallbackLocale = req.fallbackLocale
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
const options = {
id: args.id,
collection,
depth: 0,
draft: args.draft,
req: isolateObjectProperty(req, 'transactionID'),
}
const result = await duplicateOperation(options)
return result
}
}

View File

@@ -27,6 +27,7 @@ import verifyEmail from '../resolvers/auth/verifyEmail.js'
import createResolver from '../resolvers/collections/create.js' import createResolver from '../resolvers/collections/create.js'
import getDeleteResolver from '../resolvers/collections/delete.js' import getDeleteResolver from '../resolvers/collections/delete.js'
import { docAccessResolver } from '../resolvers/collections/docAccess.js' import { docAccessResolver } from '../resolvers/collections/docAccess.js'
import duplicateResolver from '../resolvers/collections/duplicate.js'
import findResolver from '../resolvers/collections/find.js' import findResolver from '../resolvers/collections/find.js'
import findByIDResolver from '../resolvers/collections/findByID.js' import findByIDResolver from '../resolvers/collections/findByID.js'
import findVersionByIDResolver from '../resolvers/collections/findVersionByID.js' import findVersionByIDResolver from '../resolvers/collections/findVersionByID.js'
@@ -237,6 +238,14 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
resolve: getDeleteResolver(collection), resolve: getDeleteResolver(collection),
} }
graphqlResult.Mutation.fields[`duplicate${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: duplicateResolver(collection),
}
if (collectionConfig.versions) { if (collectionConfig.versions) {
const versionIDType = config.db.defaultIDType === 'text' ? GraphQLString : GraphQLInt const versionIDType = config.db.defaultIDType === 'text' ? GraphQLString : GraphQLInt
const versionCollectionFields: Field[] = [ const versionCollectionFields: Field[] = [

View File

@@ -0,0 +1,33 @@
import { getTranslation } from '@payloadcms/translations'
import httpStatus from 'http-status'
import { duplicateOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const duplicate: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
// draft defaults to true, unless explicitly set requested as false to prevent the newly duplicated document from being published
const draft = searchParams.get('draft') !== 'false'
const doc = await duplicateOperation({
id,
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft,
req,
})
const message = req.t('general:successfullyDuplicated', {label: getTranslation(collection.config.labels.singular, req.i18n)})
return Response.json(
{
doc,
message,
},
{
status: httpStatus.OK,
},
)
}

View File

@@ -44,6 +44,7 @@ import { findVersionByID as findVersionByIdGlobal } from './globals/findVersionB
import { findVersions as findVersionsGlobal } from './globals/findVersions.js' import { findVersions as findVersionsGlobal } from './globals/findVersions.js'
import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js' import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js'
import { update as updateGlobal } from './globals/update.js' import { update as updateGlobal } from './globals/update.js'
import { duplicate } from './collections/duplicate.js'
const endpoints = { const endpoints = {
collection: { collection: {
@@ -71,6 +72,7 @@ const endpoints = {
'doc-access-by-id': docAccess, 'doc-access-by-id': docAccess,
'doc-verify-by-id': verifyEmail, 'doc-verify-by-id': verifyEmail,
'doc-versions-by-id': restoreVersion, 'doc-versions-by-id': restoreVersion,
duplicate,
'first-register': registerFirstUser, 'first-register': registerFirstUser,
'forgot-password': forgotPassword, 'forgot-password': forgotPassword,
login, login,
@@ -356,6 +358,9 @@ export const POST =
res = await ( res = await (
endpoints.collection.POST[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID endpoints.collection.POST[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
)({ id: slug3, collection, req }) )({ id: slug3, collection, req })
} else if (slug3 === 'duplicate') {
// /:collection/:id/duplicate
res = await endpoints.collection.POST.duplicate({ id: slug2, collection, req })
} }
break break
} }

View File

@@ -98,6 +98,9 @@ const sanitizeCollection = (
if (sanitized.upload) { if (sanitized.upload) {
if (sanitized.upload === true) sanitized.upload = {} if (sanitized.upload === true) sanitized.upload = {}
// disable duplicate for uploads by default
sanitized.admin.disableDuplicate = sanitized.admin.disableDuplicate || true
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
sanitized.admin.useAsTitle = sanitized.admin.useAsTitle =
sanitized.admin.useAsTitle && sanitized.admin.useAsTitle !== 'id' sanitized.admin.useAsTitle && sanitized.admin.useAsTitle !== 'id'
@@ -136,6 +139,9 @@ const sanitizeCollection = (
} }
} }
// disable duplicate for auth enabled collections by default
sanitized.admin.disableDuplicate = sanitized.admin.disableDuplicate || true
if (!sanitized.auth.strategies) { if (!sanitized.auth.strategies) {
sanitized.auth.strategies = [] sanitized.auth.strategies = []
} }

View File

@@ -1,11 +1,7 @@
import joi from 'joi' import joi from 'joi'
import { endpointsSchema } from '../../config/schema.js' import { endpointsSchema } from '../../config/schema.js'
import { import { componentSchema, customViewSchema, livePreviewSchema, } from '../../config/shared/componentSchema.js'
componentSchema,
customViewSchema,
livePreviewSchema,
} from '../../config/shared/componentSchema.js'
const strategyBaseSchema = joi.object().keys({ const strategyBaseSchema = joi.object().keys({
logout: joi.boolean(), logout: joi.boolean(),
@@ -65,9 +61,6 @@ const collectionSchema = joi.object().keys({
group: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])), group: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
hidden: joi.alternatives().try(joi.boolean(), joi.func()), hidden: joi.alternatives().try(joi.boolean(), joi.func()),
hideAPIURL: joi.bool(), hideAPIURL: joi.bool(),
hooks: joi.object({
beforeDuplicate: joi.func(),
}),
listSearchableFields: joi.array().items(joi.string()), listSearchableFields: joi.array().items(joi.string()),
livePreview: joi.object(livePreviewSchema), livePreview: joi.object(livePreviewSchema),
pagination: joi.object({ pagination: joi.object({

View File

@@ -195,15 +195,6 @@ export type AfterForgotPasswordHook = (args: {
context: RequestContext context: RequestContext
}) => any }) => any
type BeforeDuplicateArgs<T> = {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
data: T
locale?: string
}
export type BeforeDuplicate<T = any> = (args: BeforeDuplicateArgs<T>) => Promise<T> | T
export type CollectionAdminOptions = { export type CollectionAdminOptions = {
/** /**
* Custom admin components * Custom admin components
@@ -275,12 +266,6 @@ export type CollectionAdminOptions = {
* Hide the API URL within the Edit view * Hide the API URL within the Edit view
*/ */
hideAPIURL?: boolean hideAPIURL?: boolean
hooks?: {
/**
* Function that allows you to modify a document's data before it is duplicated
*/
beforeDuplicate?: BeforeDuplicate
}
/** /**
* Additional fields to be searched via the full text search * Additional fields to be searched via the full text search
*/ */

View File

@@ -0,0 +1,366 @@
import type { DeepPartial } from 'ts-essentials'
import httpStatus from 'http-status'
import type { FindOneArgs } from '../../database/types.js'
import type { GeneratedTypes } from '../../index.js'
import type { PayloadRequest } from '../../types/index.js'
import type { Collection } from '../config/types.js'
import executeAccess from '../../auth/executeAccess.js'
import { hasWhereAccessResult } from '../../auth/types.js'
import { combineQueries } from '../../database/combineQueries.js'
import { APIError, Forbidden, NotFound } 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 { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion.js'
import { saveVersion } from '../../versions/saveVersion.js'
import { buildAfterOperation } from './utils.js'
export type Arguments = {
collection: Collection
depth?: number
draft?: boolean
id: number | string
overrideAccess?: boolean
req: PayloadRequest
showHiddenFields?: boolean
}
export const duplicateOperation = async <TSlug extends keyof GeneratedTypes['collections']>(
incomingArgs: Arguments,
): Promise<GeneratedTypes['collections'][TSlug]> => {
let args = incomingArgs
try {
const shouldCommit = await initTransaction(args.req)
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'update',
req: args.req,
})) || args
}, Promise.resolve())
const {
id,
collection: { config: collectionConfig },
depth,
draft: draftArg = true,
overrideAccess,
req: {
fallbackLocale,
payload: { config },
payload,
},
req,
showHiddenFields,
} = args
if (!id) {
throw new APIError('Missing ID of document to duplicate.', httpStatus.BAD_REQUEST)
}
const shouldSaveDraft = Boolean(draftArg && collectionConfig.versions.drafts)
// /////////////////////////////////////
// Read Access
// /////////////////////////////////////
const accessResults = !overrideAccess
? await executeAccess({ id, req }, collectionConfig.access.read)
: true
const hasWherePolicy = hasWhereAccessResult(accessResults)
// /////////////////////////////////////
// Retrieve document
// /////////////////////////////////////
const findOneArgs: FindOneArgs = {
collection: collectionConfig.slug,
locale: req.locale,
req,
where: combineQueries({ id: { equals: id } }, accessResults),
}
const docWithLocales = await getLatestCollectionVersion({
id,
config: collectionConfig,
payload,
query: findOneArgs,
req,
})
if (!docWithLocales && !hasWherePolicy) throw new NotFound(req.t)
if (!docWithLocales && hasWherePolicy) throw new Forbidden(req.t)
// remove the createdAt timestamp and rely on the db to default it
delete docWithLocales.createdAt
// for version enabled collections, override the current status with draft, unless draft is explicitly set to false
if (shouldSaveDraft) {
docWithLocales._status = 'draft'
}
// /////////////////////////////////////
// Iterate locales of document and call the db create or update functions
// /////////////////////////////////////
let locales = [undefined]
let versionDoc
if (config.localization) {
locales = config.localization.locales.map(({ code }) => code)
}
await locales.reduce(async (previousPromise, locale: string | undefined, i) => {
await previousPromise
const operation = i === 0 ? 'create' : 'update'
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: docWithLocales,
fallbackLocale: null,
global: null,
locale,
overrideAccess: true,
req,
showHiddenFields: true,
})
// /////////////////////////////////////
// Create Access
// /////////////////////////////////////
if (operation === 'create' && !overrideAccess) {
await executeAccess({ data: originalDoc, req }, collectionConfig.access.create)
}
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
let data = await beforeValidate<DeepPartial<GeneratedTypes['collections'][TSlug]>>({
id,
collection: collectionConfig,
context: req.context,
data: originalDoc,
doc: originalDoc,
duplicate: true,
global: null,
operation,
overrideAccess,
req,
})
// /////////////////////////////////////
// beforeValidate - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation,
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation,
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
const result = await beforeChange<GeneratedTypes['collections'][TSlug]>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
docWithLocales,
global: null,
operation,
req,
skipValidation: shouldSaveDraft,
})
// /////////////////////////////////////
// Handle potential password update
// /////////////////////////////////////
// const dataToUpdate: Record<string, unknown> = { ...result }
// if (shouldSavePassword && typeof password === 'string') {
// const { hash, salt } = await generatePasswordSaltHash({ password })
// dataToUpdate.salt = salt
// dataToUpdate.hash = hash
// delete dataToUpdate.password
// delete data.password
// }
// /////////////////////////////////////
// Create / Update
// /////////////////////////////////////
if (i === 0) {
versionDoc = await payload.db.create({
collection: collectionConfig.slug,
data: result,
req,
})
} else {
versionDoc = await req.payload.db.updateOne({
id: versionDoc.id,
collection: collectionConfig.slug,
data: result,
locale,
req,
})
}
}, Promise.resolve())
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
let result = versionDoc
if (collectionConfig.versions) {
result = await saveVersion({
id: versionDoc.id,
collection: collectionConfig,
docWithLocales: {
...versionDoc,
createdAt: docWithLocales.createdAt,
},
draft: shouldSaveDraft,
payload,
req,
})
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: versionDoc,
fallbackLocale,
global: null,
locale: req.locale,
overrideAccess,
req,
showHiddenFields,
})
// /////////////////////////////////////
// afterRead - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange<GeneratedTypes['collections'][TSlug]>({
collection: collectionConfig,
context: req.context,
data: versionDoc,
doc: result,
global: null,
operation: 'create',
previousDoc: {},
req,
})
// /////////////////////////////////////
// afterChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'create',
previousDoc: {},
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
args,
collection: collectionConfig,
operation: 'create',
result,
})
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
if (shouldCommit) await commitTransaction(req)
return result
} catch (error: unknown) {
await killTransaction(args.req)
throw error
}
}

View File

@@ -0,0 +1,57 @@
import type { GeneratedTypes } from '../../..//index.js'
import type { Payload } from '../../../index.js'
import type { Document, PayloadRequest, RequestContext } from '../../../types/index.js'
import { APIError } from '../../../errors/index.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js'
import { duplicateOperation } from '../duplicate.js'
export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
collection: TSlug
/**
* context, which will then be passed to req.context, which can be read by hooks
*/
context?: RequestContext
depth?: number
draft?: boolean
fallbackLocale?: string
id: number | string
locale?: string
overrideAccess?: boolean
req?: PayloadRequest
showHiddenFields?: boolean
user?: Document
}
export async function duplicate<TSlug extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<TSlug>,
): Promise<GeneratedTypes['collections'][TSlug]> {
const {
id,
collection: collectionSlug,
depth,
draft,
overrideAccess = true,
showHiddenFields,
} = options
const collection = payload.collections[collectionSlug]
if (!collection) {
throw new APIError(
`The collection with slug ${String(collectionSlug)} can't be found. Duplicate Operation.`,
)
}
const req = await createLocalReq(options, payload)
return duplicateOperation<TSlug>({
id,
collection,
depth,
draft,
overrideAccess,
req,
showHiddenFields,
})
}

View File

@@ -1,6 +1,7 @@
import auth from '../../../auth/operations/local/index.js' import auth from '../../../auth/operations/local/index.js'
import create from './create.js' import create from './create.js'
import deleteLocal from './delete.js' import deleteLocal from './delete.js'
import { duplicate } from './duplicate.js'
import find from './find.js' import find from './find.js'
import findByID from './findByID.js' import findByID from './findByID.js'
import findVersionByID from './findVersionByID.js' import findVersionByID from './findVersionByID.js'
@@ -12,6 +13,7 @@ export default {
auth, auth,
create, create,
deleteLocal, deleteLocal,
duplicate,
find, find,
findByID, findByID,
findVersionByID, findVersionByID,

View File

@@ -27,7 +27,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
export type ServerOnlyCollectionAdminProperties = keyof Pick< export type ServerOnlyCollectionAdminProperties = keyof Pick<
SanitizedCollectionConfig['admin'], SanitizedCollectionConfig['admin'],
'components' | 'hidden' | 'hooks' | 'preview' 'components' | 'hidden' | 'preview'
> >
export type ServerOnlyGlobalProperties = keyof Pick< export type ServerOnlyGlobalProperties = keyof Pick<
@@ -193,7 +193,6 @@ const sanitizeCollections = (
'components', 'components',
'hidden', 'hidden',
'preview', 'preview',
'hooks',
// `livePreview` is handled separately // `livePreview` is handled separately
] ]

View File

@@ -14,6 +14,7 @@ export { createOperation } from '../collections/operations/create.js'
export { deleteOperation } from '../collections/operations/delete.js' export { deleteOperation } from '../collections/operations/delete.js'
export { deleteByIDOperation } from '../collections/operations/deleteByID.js' export { deleteByIDOperation } from '../collections/operations/deleteByID.js'
export { docAccessOperation } from '../collections/operations/docAccess.js' export { docAccessOperation } from '../collections/operations/docAccess.js'
export { duplicateOperation } from '../collections/operations/duplicate.js'
export { findOperation } from '../collections/operations/find.js' export { findOperation } from '../collections/operations/find.js'
export { findByIDOperation } from '../collections/operations/findByID.js' export { findByIDOperation } from '../collections/operations/findByID.js'
export { findVersionByIDOperation } from '../collections/operations/findVersionByID.js' export { findVersionByIDOperation } from '../collections/operations/findVersionByID.js'

View File

@@ -25,7 +25,6 @@ export type {
AfterReadHook as CollectionAfterReadHook, AfterReadHook as CollectionAfterReadHook,
BeforeChangeHook as CollectionBeforeChangeHook, BeforeChangeHook as CollectionBeforeChangeHook,
BeforeDeleteHook as CollectionBeforeDeleteHook, BeforeDeleteHook as CollectionBeforeDeleteHook,
BeforeDuplicate,
BeforeLoginHook as CollectionBeforeLoginHook, BeforeLoginHook as CollectionBeforeLoginHook,
BeforeOperationHook as CollectionBeforeOperationHook, BeforeOperationHook as CollectionBeforeOperationHook,
BeforeReadHook as CollectionBeforeReadHook, BeforeReadHook as CollectionBeforeReadHook,

View File

@@ -10,6 +10,7 @@ import {
import { formatLabels, toWords } from '../../utilities/formatLabels.js' import { formatLabels, toWords } from '../../utilities/formatLabels.js'
import { baseBlockFields } from '../baseFields/baseBlockFields.js' import { baseBlockFields } from '../baseFields/baseBlockFields.js'
import { baseIDField } from '../baseFields/baseIDField.js' import { baseIDField } from '../baseFields/baseIDField.js'
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
import validations from '../validations.js' import validations from '../validations.js'
import { fieldAffectsData, tabHasName } from './types.js' import { fieldAffectsData, tabHasName } from './types.js'
@@ -129,6 +130,8 @@ export const sanitizeFields = ({
if (!field.hooks) field.hooks = {} if (!field.hooks) field.hooks = {}
if (!field.access) field.access = {} if (!field.access) field.access = {}
setDefaultBeforeDuplicate(field)
} }
if (!field.admin) { if (!field.admin) {

View File

@@ -45,6 +45,7 @@ export const baseField = joi
afterChange: joi.array().items(joi.func()).default([]), afterChange: joi.array().items(joi.func()).default([]),
afterRead: joi.array().items(joi.func()).default([]), afterRead: joi.array().items(joi.func()).default([]),
beforeChange: joi.array().items(joi.func()).default([]), beforeChange: joi.array().items(joi.func()).default([]),
beforeDuplicate: joi.array().items(joi.func()).default([]),
beforeValidate: joi.array().items(joi.func()).default([]), beforeValidate: joi.array().items(joi.func()).default([]),
}) })
.default(), .default(),

View File

@@ -183,6 +183,10 @@ export interface FieldBase {
afterChange?: FieldHook[] afterChange?: FieldHook[]
afterRead?: FieldHook[] afterRead?: FieldHook[]
beforeChange?: FieldHook[] beforeChange?: FieldHook[]
/**
* Runs before a document is duplicated to prevent errors in unique fields or return null to use defaultValue.
*/
beforeDuplicate?: FieldHook[]
beforeValidate?: FieldHook[] beforeValidate?: FieldHook[]
} }
index?: boolean index?: boolean

View File

@@ -10,6 +10,7 @@ type Args<T> = {
context: RequestContext context: RequestContext
data: Record<string, unknown> | T data: Record<string, unknown> | T
doc?: Record<string, unknown> | T doc?: Record<string, unknown> | T
duplicate?: boolean
global: SanitizedGlobalConfig | null global: SanitizedGlobalConfig | null
id?: number | string id?: number | string
operation: 'create' | 'update' operation: 'create' | 'update'
@@ -23,6 +24,7 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
context, context,
data: incomingData, data: incomingData,
doc, doc,
duplicate = false,
global, global,
operation, operation,
overrideAccess, overrideAccess,
@@ -36,6 +38,7 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
context, context,
data, data,
doc, doc,
duplicate,
fields: collection?.fields || global?.fields, fields: collection?.fields || global?.fields,
global, global,
operation, operation,

View File

@@ -15,6 +15,7 @@ type Args<T> = {
context: RequestContext context: RequestContext
data: T data: T
doc: T doc: T
duplicate: boolean
field: Field | TabAsField field: Field | TabAsField
global: SanitizedGlobalConfig | null global: SanitizedGlobalConfig | null
id?: number | string id?: number | string
@@ -38,6 +39,7 @@ export const promise = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
field, field,
global, global,
operation, operation,
@@ -259,6 +261,32 @@ export const promise = async <T>({
}) })
} }
} }
// Execute beforeDuplicate hook
if (duplicate && field.hooks?.beforeDuplicate) {
if (field.hooks?.beforeDuplicate) {
await field.hooks.beforeDuplicate.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
collection,
context,
data,
field,
global,
operation,
originalDoc: doc,
req,
siblingData,
value: siblingData[field.name],
})
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
} }
// Traverse subfields // Traverse subfields
@@ -276,6 +304,7 @@ export const promise = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
fields: field.fields, fields: field.fields,
global, global,
operation, operation,
@@ -301,6 +330,7 @@ export const promise = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
fields: field.fields, fields: field.fields,
global, global,
operation, operation,
@@ -336,6 +366,7 @@ export const promise = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
fields: block.fields, fields: block.fields,
global, global,
operation, operation,
@@ -361,6 +392,7 @@ export const promise = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
fields: field.fields, fields: field.fields,
global, global,
operation, operation,
@@ -393,6 +425,7 @@ export const promise = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
fields: field.fields, fields: field.fields,
global, global,
operation, operation,
@@ -412,6 +445,7 @@ export const promise = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
global, global,
operation, operation,

View File

@@ -10,6 +10,7 @@ type Args<T> = {
context: RequestContext context: RequestContext
data: T data: T
doc: T doc: T
duplicate: boolean
fields: (Field | TabAsField)[] fields: (Field | TabAsField)[]
global: SanitizedGlobalConfig | null global: SanitizedGlobalConfig | null
id?: number | string id?: number | string
@@ -26,6 +27,7 @@ export const traverseFields = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
fields, fields,
global, global,
operation, operation,
@@ -43,6 +45,7 @@ export const traverseFields = async <T>({
context, context,
data, data,
doc, doc,
duplicate,
field, field,
global, global,
operation, operation,

View File

@@ -0,0 +1,33 @@
import { extractTranslations } from '../translations/extractTranslations.js'
const copyTranslations = extractTranslations(['general:copy'])
// default beforeDuplicate hook for required and unique fields
import type { FieldAffectingData, FieldHook } from './config/types.js'
const unique: FieldHook = ({ value }) => (typeof value === 'string' ? `${value} - Copy` : undefined)
const localizedUnique: FieldHook = ({ req, value}) =>
(value ? `${value} - ${copyTranslations?.[req.locale]?.['general:copy'] ?? 'Copy'}` : undefined)
const uniqueRequired: FieldHook = ({ value }) => (`${value} - Copy`)
const localizedUniqueRequired: FieldHook = ({ req, value }) => (`${value} - ${copyTranslations?.[req.locale]?.['general:copy'] ?? 'Copy'}`)
export const setDefaultBeforeDuplicate = (field: FieldAffectingData) => {
if (
(('required' in field && field.required) || field.unique) &&
(!field.hooks?.beforeDuplicate || Array.isArray(field.hooks.beforeDuplicate) && field.hooks.beforeDuplicate.length === 0)
) {
if ((field.type === 'text' || field.type === 'textarea') && field.required && field.unique) {
field.hooks.beforeDuplicate = [
field.localized
? localizedUniqueRequired
: uniqueRequired
]
} else if (field.unique) {
field.hooks.beforeDuplicate = [
field.localized
? localizedUnique
: unique
]
}
}
}

View File

@@ -26,6 +26,7 @@ import type { Options as FindByIDOptions } from './collections/operations/local/
import type { Options as FindVersionByIDOptions } from './collections/operations/local/findVersionByID.js' import type { Options as FindVersionByIDOptions } from './collections/operations/local/findVersionByID.js'
import type { Options as FindVersionsOptions } from './collections/operations/local/findVersions.js' import type { Options as FindVersionsOptions } from './collections/operations/local/findVersions.js'
import type { Options as RestoreVersionOptions } from './collections/operations/local/restoreVersion.js' import type { Options as RestoreVersionOptions } from './collections/operations/local/restoreVersion.js'
import type { Options as DuplicateOptions } from './collections/operations/local/duplicate.js'
import type { import type {
ByIDOptions as UpdateByIDOptions, ByIDOptions as UpdateByIDOptions,
ManyOptions as UpdateManyOptions, ManyOptions as UpdateManyOptions,
@@ -411,6 +412,13 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
const { update } = localOperations const { update } = localOperations
return update<T>(this, options) return update<T>(this, options)
} }
duplicate = async <T extends keyof TGeneratedTypes['collections']>(
options: DuplicateOptions<T>,
): Promise<TGeneratedTypes['collections'][T]> => {
const { duplicate } = localOperations
return duplicate<T>(this, options)
}
} }
const initialized = new BasePayload() const initialized = new BasePayload()

View File

@@ -38,12 +38,14 @@ export default {
textToDisplay: 'النصّ الذي تريد إظهاره', textToDisplay: 'النصّ الذي تريد إظهاره',
}, },
general: { general: {
copy: 'نسخ',
createdAt: 'تمّ الإنشاء في', createdAt: 'تمّ الإنشاء في',
deletedCountSuccessfully: 'تمّ حذف {{count}} {{label}} بنجاح.', deletedCountSuccessfully: 'تمّ حذف {{count}} {{label}} بنجاح.',
deletedSuccessfully: 'تمّ الحذف بنجاح.', deletedSuccessfully: 'تمّ الحذف بنجاح.',
email: 'البريد الإلكتروني', email: 'البريد الإلكتروني',
notFound: 'غير موجود', notFound: 'غير موجود',
successfullyCreated: '{{label}} تم إنشاؤها بنجاح.', successfullyCreated: '{{label}} تم إنشاؤها بنجاح.',
successfullyDuplicated: '{{label}} تم استنساخها بنجاح.',
thisLanguage: 'العربية', thisLanguage: 'العربية',
updatedAt: 'تم التحديث في', updatedAt: 'تم التحديث في',
updatedCountSuccessfully: 'تم تحديث {{count}} {{label}} بنجاح.', updatedCountSuccessfully: 'تم تحديث {{count}} {{label}} بنجاح.',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'Göstəriləcək mətn', textToDisplay: 'Göstəriləcək mətn',
}, },
general: { general: {
copy: 'Kopyala',
createdAt: 'Yaradıldığı tarix', createdAt: 'Yaradıldığı tarix',
deletedCountSuccessfully: '{{count}} {{label}} uğurla silindi.', deletedCountSuccessfully: '{{count}} {{label}} uğurla silindi.',
deletedSuccessfully: 'Uğurla silindi.', deletedSuccessfully: 'Uğurla silindi.',
email: 'Elektron poçt', email: 'Elektron poçt',
notFound: 'Tapılmadı', notFound: 'Tapılmadı',
successfullyCreated: '{{label}} uğurla yaradıldı.', successfullyCreated: '{{label}} uğurla yaradıldı.',
successfullyDuplicated: '{{label}} uğurla dublikatlandı.',
thisLanguage: 'Azərbaycan dili', thisLanguage: 'Azərbaycan dili',
updatedAt: 'Yeniləndiyi tarix', updatedAt: 'Yeniləndiyi tarix',
updatedCountSuccessfully: '{{count}} {{label}} uğurla yeniləndi.', updatedCountSuccessfully: '{{count}} {{label}} uğurla yeniləndi.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Текст към дисплей', textToDisplay: 'Текст към дисплей',
}, },
general: { general: {
copy: 'Копирай',
createdAt: 'Създаден на', createdAt: 'Създаден на',
deletedCountSuccessfully: 'Изтрити {{count}} {{label}} успешно.', deletedCountSuccessfully: 'Изтрити {{count}} {{label}} успешно.',
deletedSuccessfully: 'Изтрито успешно.', deletedSuccessfully: 'Изтрито успешно.',
email: 'Имейл', email: 'Имейл',
notFound: 'Няма открит', notFound: 'Няма открит',
successfullyCreated: '{{label}} успешно създаден.', successfullyCreated: '{{label}} успешно създаден.',
successfullyDuplicated: '{{label}} успешно дупликиран.',
thisLanguage: 'Български', thisLanguage: 'Български',
updatedAt: 'Обновен на', updatedAt: 'Обновен на',
updatedCountSuccessfully: 'Обновени {{count}} {{label}} успешно.', updatedCountSuccessfully: 'Обновени {{count}} {{label}} успешно.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Text k zobrazení', textToDisplay: 'Text k zobrazení',
}, },
general: { general: {
copy: 'Kopírovat',
createdAt: 'Vytvořeno v', createdAt: 'Vytvořeno v',
deletedCountSuccessfully: 'Úspěšně smazáno {{count}} {{label}}.', deletedCountSuccessfully: 'Úspěšně smazáno {{count}} {{label}}.',
deletedSuccessfully: 'Úspěšně odstraněno.', deletedSuccessfully: 'Úspěšně odstraněno.',
email: 'E-mail', email: 'E-mail',
notFound: 'Nenalezeno', notFound: 'Nenalezeno',
successfullyCreated: '{{label}} úspěšně vytvořeno.', successfullyCreated: '{{label}} úspěšně vytvořeno.',
successfullyDuplicated: '{{label}} úspěšně duplikováno.',
thisLanguage: 'Čeština', thisLanguage: 'Čeština',
updatedAt: 'Aktualizováno v', updatedAt: 'Aktualizováno v',
updatedCountSuccessfully: 'Úspěšně aktualizováno {{count}} {{label}}.', updatedCountSuccessfully: 'Úspěšně aktualizováno {{count}} {{label}}.',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'Angezeigter Text', textToDisplay: 'Angezeigter Text',
}, },
general: { general: {
copy: 'Kopieren',
createdAt: 'Erstellt am', createdAt: 'Erstellt am',
deletedCountSuccessfully: '{{count}} {{label}} erfolgreich gelöscht.', deletedCountSuccessfully: '{{count}} {{label}} erfolgreich gelöscht.',
deletedSuccessfully: 'Erfolgreich gelöscht.', deletedSuccessfully: 'Erfolgreich gelöscht.',
email: 'E-Mail', email: 'E-Mail',
notFound: 'Nicht gefunden', notFound: 'Nicht gefunden',
successfullyCreated: '{{label}} erfolgreich erstellt.', successfullyCreated: '{{label}} erfolgreich erstellt.',
successfullyDuplicated: '{{label}} wurde erfolgreich dupliziert.',
thisLanguage: 'Deutsch', thisLanguage: 'Deutsch',
updatedAt: 'Aktualisiert am', updatedAt: 'Aktualisiert am',
updatedCountSuccessfully: '{{count}} {{label}} erfolgreich aktualisiert.', updatedCountSuccessfully: '{{count}} {{label}} erfolgreich aktualisiert.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Text to display', textToDisplay: 'Text to display',
}, },
general: { general: {
copy: 'Copy',
createdAt: 'Created At', createdAt: 'Created At',
deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.', deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.',
deletedSuccessfully: 'Deleted successfully.', deletedSuccessfully: 'Deleted successfully.',
email: 'Email', email: 'Email',
notFound: 'Not Found', notFound: 'Not Found',
successfullyCreated: '{{label}} successfully created.', successfullyCreated: '{{label}} successfully created.',
successfullyDuplicated: '{{label}} successfully duplicated.',
thisLanguage: 'English', thisLanguage: 'English',
updatedAt: 'Updated At', updatedAt: 'Updated At',
updatedCountSuccessfully: 'Updated {{count}} {{label}} successfully.', updatedCountSuccessfully: 'Updated {{count}} {{label}} successfully.',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'Texto a mostrar', textToDisplay: 'Texto a mostrar',
}, },
general: { general: {
copy: 'Copiar',
createdAt: 'Fecha de creación', createdAt: 'Fecha de creación',
deletedCountSuccessfully: 'Se eliminó {{count}} {{label}} con éxito.', deletedCountSuccessfully: 'Se eliminó {{count}} {{label}} con éxito.',
deletedSuccessfully: 'Borrado exitosamente.', deletedSuccessfully: 'Borrado exitosamente.',
email: 'Correo electrónico', email: 'Correo electrónico',
notFound: 'No encontrado', notFound: 'No encontrado',
successfullyCreated: '{{label}} creado correctamente.', successfullyCreated: '{{label}} creado correctamente.',
successfullyDuplicated: '{{label}} duplicado correctamente.',
thisLanguage: 'Español', thisLanguage: 'Español',
updatedAt: 'Fecha de modificado', updatedAt: 'Fecha de modificado',
updatedCountSuccessfully: '{{count}} {{label}} actualizado con éxito.', updatedCountSuccessfully: '{{count}} {{label}} actualizado con éxito.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'متن برای نمایش', textToDisplay: 'متن برای نمایش',
}, },
general: { general: {
copy: 'رونوشت',
createdAt: 'ساخته شده در', createdAt: 'ساخته شده در',
deletedCountSuccessfully: 'تعداد {{count}} {{label}} با موفقیت پاک گردید.', deletedCountSuccessfully: 'تعداد {{count}} {{label}} با موفقیت پاک گردید.',
deletedSuccessfully: 'با موفقیت حذف شد.', deletedSuccessfully: 'با موفقیت حذف شد.',
email: 'رایانامه', email: 'رایانامه',
notFound: 'یافت نشد', notFound: 'یافت نشد',
successfullyCreated: '{{label}} با موفقیت ساخته شد.', successfullyCreated: '{{label}} با موفقیت ساخته شد.',
successfullyDuplicated: '{{label}} با موفقیت رونوشت شد.',
thisLanguage: 'فارسی', thisLanguage: 'فارسی',
updatedAt: 'بروز شده در', updatedAt: 'بروز شده در',
updatedCountSuccessfully: 'تعداد {{count}} با عنوان {{label}} با موفقیت بروزرسانی شدند.', updatedCountSuccessfully: 'تعداد {{count}} با عنوان {{label}} با موفقیت بروزرسانی شدند.',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'Texte à afficher', textToDisplay: 'Texte à afficher',
}, },
general: { general: {
copy: 'Copie',
createdAt: 'Créé(e) à', createdAt: 'Créé(e) à',
deletedCountSuccessfully: '{{count}} {{label}} supprimé avec succès.', deletedCountSuccessfully: '{{count}} {{label}} supprimé avec succès.',
deletedSuccessfully: 'Supprimé(e) avec succès.', deletedSuccessfully: 'Supprimé(e) avec succès.',
email: 'E-mail', email: 'E-mail',
notFound: 'Pas trouvé', notFound: 'Pas trouvé',
successfullyCreated: '{{label}} créé(e) avec succès.', successfullyCreated: '{{label}} créé(e) avec succès.',
successfullyDuplicated: '{{label}} dupliqué(e) avec succès.',
thisLanguage: 'Français', thisLanguage: 'Français',
updatedAt: 'Modifié le', updatedAt: 'Modifié le',
updatedCountSuccessfully: '{{count}} {{label}} mis à jour avec succès.', updatedCountSuccessfully: '{{count}} {{label}} mis à jour avec succès.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Tekst za prikaz', textToDisplay: 'Tekst za prikaz',
}, },
general: { general: {
copy: 'Kopiraj',
createdAt: 'Kreirano u', createdAt: 'Kreirano u',
deletedCountSuccessfully: 'Uspješno izbrisano {{count}} {{label}}.', deletedCountSuccessfully: 'Uspješno izbrisano {{count}} {{label}}.',
deletedSuccessfully: 'Uspješno obrisano.', deletedSuccessfully: 'Uspješno obrisano.',
email: 'Email', email: 'Email',
notFound: 'Nije pronađeno', notFound: 'Nije pronađeno',
successfullyCreated: '{{label}} uspješno kreirano.', successfullyCreated: '{{label}} uspješno kreirano.',
successfullyDuplicated: '{{label}} uspješno duplicirano.',
thisLanguage: 'Hrvatski', thisLanguage: 'Hrvatski',
updatedAt: 'Ažurirano u', updatedAt: 'Ažurirano u',
updatedCountSuccessfully: 'Uspješno ažurirano {{count}} {{label}}.', updatedCountSuccessfully: 'Uspješno ažurirano {{count}} {{label}}.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Megjelenítendő szöveg', textToDisplay: 'Megjelenítendő szöveg',
}, },
general: { general: {
copy: 'Másolás',
createdAt: 'Létrehozva:', createdAt: 'Létrehozva:',
deletedCountSuccessfully: '{{count}} {{label}} sikeresen törölve.', deletedCountSuccessfully: '{{count}} {{label}} sikeresen törölve.',
deletedSuccessfully: 'Sikeresen törölve.', deletedSuccessfully: 'Sikeresen törölve.',
email: 'E-mail', email: 'E-mail',
notFound: 'Nem található', notFound: 'Nem található',
successfullyCreated: '{{label}} sikeresen létrehozva.', successfullyCreated: '{{label}} sikeresen létrehozva.',
successfullyDuplicated: '{{label}} sikeresen duplikálódott.',
thisLanguage: 'Magyar', thisLanguage: 'Magyar',
updatedAt: 'Frissítve:', updatedAt: 'Frissítve:',
updatedCountSuccessfully: '{{count}} {{label}} sikeresen frissítve.', updatedCountSuccessfully: '{{count}} {{label}} sikeresen frissítve.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Testo da visualizzare', textToDisplay: 'Testo da visualizzare',
}, },
general: { general: {
copy: 'Copia',
createdAt: 'Creato il', createdAt: 'Creato il',
deletedCountSuccessfully: '{{count}} {{label}} eliminato con successo.', deletedCountSuccessfully: '{{count}} {{label}} eliminato con successo.',
deletedSuccessfully: 'Eliminato con successo.', deletedSuccessfully: 'Eliminato con successo.',
email: 'Email', email: 'Email',
notFound: 'Non Trovato', notFound: 'Non Trovato',
successfullyCreated: '{{label}} creato con successo.', successfullyCreated: '{{label}} creato con successo.',
successfullyDuplicated: '{{label}} duplicato con successo.',
thisLanguage: 'Italiano', thisLanguage: 'Italiano',
updatedAt: 'Aggiornato il', updatedAt: 'Aggiornato il',
updatedCountSuccessfully: '{{count}} {{label}} aggiornato con successo.', updatedCountSuccessfully: '{{count}} {{label}} aggiornato con successo.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: '表示するテキスト', textToDisplay: '表示するテキスト',
}, },
general: { general: {
copy: 'コピー',
createdAt: '作成日', createdAt: '作成日',
deletedCountSuccessfully: '{{count}}つの{{label}}を正常に削除しました。', deletedCountSuccessfully: '{{count}}つの{{label}}を正常に削除しました。',
deletedSuccessfully: '正常に削除されました。', deletedSuccessfully: '正常に削除されました。',
email: 'メールアドレス', email: 'メールアドレス',
notFound: 'Not Found', notFound: 'Not Found',
successfullyCreated: '{{label}} が作成されました。', successfullyCreated: '{{label}} が作成されました。',
successfullyDuplicated: '{{label}} が複製されました。',
thisLanguage: 'Japanese', thisLanguage: 'Japanese',
updatedAt: '更新日', updatedAt: '更新日',
updatedCountSuccessfully: '{{count}}つの{{label}}を正常に更新しました。', updatedCountSuccessfully: '{{count}}つの{{label}}を正常に更新しました。',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: '표시할 텍스트', textToDisplay: '표시할 텍스트',
}, },
general: { general: {
copy: '복사',
createdAt: '생성 일시', createdAt: '생성 일시',
deletedCountSuccessfully: '{{count}}개의 {{label}}를 삭제했습니다.', deletedCountSuccessfully: '{{count}}개의 {{label}}를 삭제했습니다.',
deletedSuccessfully: '삭제되었습니다.', deletedSuccessfully: '삭제되었습니다.',
email: '이메일', email: '이메일',
notFound: '찾을 수 없음', notFound: '찾을 수 없음',
successfullyCreated: '{{label}}이(가) 생성되었습니다.', successfullyCreated: '{{label}}이(가) 생성되었습니다.',
successfullyDuplicated: '{{label}}이(가) 복제되었습니다.',
thisLanguage: '한국어', thisLanguage: '한국어',
updatedAt: '업데이트 일시', updatedAt: '업데이트 일시',
updatedCountSuccessfully: '{{count}}개의 {{label}}을(를) 업데이트했습니다.', updatedCountSuccessfully: '{{count}}개의 {{label}}을(를) 업데이트했습니다.',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'ပြသရန် စာသား', textToDisplay: 'ပြသရန် စာသား',
}, },
general: { general: {
copy: 'ကူးယူမည်။',
createdAt: 'ဖန်တီးခဲ့သည့်အချိန်', createdAt: 'ဖန်တီးခဲ့သည့်အချိန်',
deletedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။', deletedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။',
deletedSuccessfully: 'အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။', deletedSuccessfully: 'အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။',
email: 'အီးမေးလ်', email: 'အီးမေးလ်',
notFound: 'ဘာမှ မရှိတော့ဘူး။', notFound: 'ဘာမှ မရှိတော့ဘူး။',
successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။', successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။',
successfullyDuplicated: '{{label}} အောင်မြင်စွာ ပုံတူပွားခဲ့သည်။',
thisLanguage: 'မြန်မာစာ', thisLanguage: 'မြန်မာစာ',
updatedAt: 'ပြင်ဆင်ခဲ့သည့်အချိန်', updatedAt: 'ပြင်ဆင်ခဲ့သည့်အချိန်',
updatedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ခဲ့သည်။', updatedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ခဲ့သည်။',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Tekst som skal vises', textToDisplay: 'Tekst som skal vises',
}, },
general: { general: {
copy: 'Kopiér',
createdAt: 'Opprettet', createdAt: 'Opprettet',
deletedCountSuccessfully: 'Slettet {{count}} {{label}}.', deletedCountSuccessfully: 'Slettet {{count}} {{label}}.',
deletedSuccessfully: 'Slettet.', deletedSuccessfully: 'Slettet.',
email: 'E-post', email: 'E-post',
notFound: 'Ikke funnet', notFound: 'Ikke funnet',
successfullyCreated: '{{label}} ble opprettet.', successfullyCreated: '{{label}} ble opprettet.',
successfullyDuplicated: '{{label}} ble duplisert.',
thisLanguage: 'Norsk', thisLanguage: 'Norsk',
updatedAt: 'Oppdatert', updatedAt: 'Oppdatert',
updatedCountSuccessfully: 'Oppdaterte {{count}} {{label}} vellykket.', updatedCountSuccessfully: 'Oppdaterte {{count}} {{label}} vellykket.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Tekst om weer te geven', textToDisplay: 'Tekst om weer te geven',
}, },
general: { general: {
copy: 'Kopiëren',
createdAt: 'Aangemaakt op', createdAt: 'Aangemaakt op',
deletedCountSuccessfully: '{{count}} {{label}} succesvol verwijderd.', deletedCountSuccessfully: '{{count}} {{label}} succesvol verwijderd.',
deletedSuccessfully: 'Succesvol verwijderd.', deletedSuccessfully: 'Succesvol verwijderd.',
email: 'E-mail', email: 'E-mail',
notFound: 'Niet gevonden', notFound: 'Niet gevonden',
successfullyCreated: '{{label}} succesvol aangemaakt.', successfullyCreated: '{{label}} succesvol aangemaakt.',
successfullyDuplicated: '{{label}} succesvol gedupliceerd.',
thisLanguage: 'Nederlands', thisLanguage: 'Nederlands',
updatedAt: 'Aangepast op', updatedAt: 'Aangepast op',
updatedCountSuccessfully: '{{count}} {{label}} succesvol bijgewerkt.', updatedCountSuccessfully: '{{count}} {{label}} succesvol bijgewerkt.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Tekst do wyświetlenia', textToDisplay: 'Tekst do wyświetlenia',
}, },
general: { general: {
copy: 'Skopiuj',
createdAt: 'Data utworzenia', createdAt: 'Data utworzenia',
deletedCountSuccessfully: 'Pomyślnie usunięto {{count}} {{label}}.', deletedCountSuccessfully: 'Pomyślnie usunięto {{count}} {{label}}.',
deletedSuccessfully: 'Pomyślnie usunięto.', deletedSuccessfully: 'Pomyślnie usunięto.',
email: 'Email', email: 'Email',
notFound: 'Nie znaleziono', notFound: 'Nie znaleziono',
successfullyCreated: 'Pomyślnie utworzono {{label}}.', successfullyCreated: 'Pomyślnie utworzono {{label}}.',
successfullyDuplicated: 'Pomyślnie zduplikowano {{label}}',
thisLanguage: 'Polski', thisLanguage: 'Polski',
updatedAt: 'Data edycji', updatedAt: 'Data edycji',
updatedCountSuccessfully: 'Pomyślnie zaktualizowano {{count}} {{label}}.', updatedCountSuccessfully: 'Pomyślnie zaktualizowano {{count}} {{label}}.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Texto a ser exibido', textToDisplay: 'Texto a ser exibido',
}, },
general: { general: {
copy: 'Copiar',
createdAt: 'Criado Em', createdAt: 'Criado Em',
deletedCountSuccessfully: 'Excluído {{count}} {{label}} com sucesso.', deletedCountSuccessfully: 'Excluído {{count}} {{label}} com sucesso.',
deletedSuccessfully: 'Apagado com sucesso.', deletedSuccessfully: 'Apagado com sucesso.',
email: 'Email', email: 'Email',
notFound: 'Não Encontrado', notFound: 'Não Encontrado',
successfullyCreated: '{{label}} criado com sucesso.', successfullyCreated: '{{label}} criado com sucesso.',
successfullyDuplicated: '{{label}} duplicado com sucesso.',
thisLanguage: 'Português', thisLanguage: 'Português',
updatedAt: 'Atualizado Em', updatedAt: 'Atualizado Em',
updatedCountSuccessfully: 'Atualizado {{count}} {{label}} com sucesso.', updatedCountSuccessfully: 'Atualizado {{count}} {{label}} com sucesso.',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'Text de afișat', textToDisplay: 'Text de afișat',
}, },
general: { general: {
copy: 'Copiați',
createdAt: 'Creat la', createdAt: 'Creat la',
deletedCountSuccessfully: 'Șterse cu succes {{count}} {{label}}.', deletedCountSuccessfully: 'Șterse cu succes {{count}} {{label}}.',
deletedSuccessfully: 'Șters cu succes.', deletedSuccessfully: 'Șters cu succes.',
email: 'Email', email: 'Email',
notFound: 'Nu a fost găsit', notFound: 'Nu a fost găsit',
successfullyCreated: '{{label}} creat(ă) cu succes.', successfullyCreated: '{{label}} creat(ă) cu succes.',
successfullyDuplicated: '{{label}} duplicat(ă) cu succes.',
thisLanguage: 'Română', thisLanguage: 'Română',
updatedAt: 'Actualizat la', updatedAt: 'Actualizat la',
updatedCountSuccessfully: 'Actualizate {{count}} {{label}} cu succes.', updatedCountSuccessfully: 'Actualizate {{count}} {{label}} cu succes.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Tekst za prikaz', textToDisplay: 'Tekst za prikaz',
}, },
general: { general: {
copy: 'Kopiraj',
createdAt: 'Kreirano u', createdAt: 'Kreirano u',
deletedCountSuccessfully: 'Uspešno izbrisano {{count}} {{label}}.', deletedCountSuccessfully: 'Uspešno izbrisano {{count}} {{label}}.',
deletedSuccessfully: 'Uspešno izbrisano.', deletedSuccessfully: 'Uspešno izbrisano.',
email: 'E-pošta', email: 'E-pošta',
notFound: 'Nije pronađeno', notFound: 'Nije pronađeno',
successfullyCreated: '{{label}} uspešno kreirano.', successfullyCreated: '{{label}} uspešno kreirano.',
successfullyDuplicated: '{{label}} uspešno duplicirano.',
thisLanguage: 'Srpski (latinica)', thisLanguage: 'Srpski (latinica)',
updatedAt: 'Ažurirano u', updatedAt: 'Ažurirano u',
updatedCountSuccessfully: 'Uspešno ažurirano {{count}} {{label}}.', updatedCountSuccessfully: 'Uspešno ažurirano {{count}} {{label}}.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Текст за приказ', textToDisplay: 'Текст за приказ',
}, },
general: { general: {
copy: 'Копирај',
createdAt: 'Креирано у', createdAt: 'Креирано у',
deletedCountSuccessfully: 'Успешно избрисано {{count}} {{label}}.', deletedCountSuccessfully: 'Успешно избрисано {{count}} {{label}}.',
deletedSuccessfully: 'Успешно избрисано.', deletedSuccessfully: 'Успешно избрисано.',
email: 'Е-пошта', email: 'Е-пошта',
notFound: 'Није пронађено', notFound: 'Није пронађено',
successfullyCreated: '{{label}} успешно креирано.', successfullyCreated: '{{label}} успешно креирано.',
successfullyDuplicated: '{{label}} успешно дуплицирано.',
thisLanguage: 'Српски (ћирилица)', thisLanguage: 'Српски (ћирилица)',
updatedAt: 'Ажурирано у', updatedAt: 'Ажурирано у',
updatedCountSuccessfully: 'Успешно ажурирано {{count}} {{label}}.', updatedCountSuccessfully: 'Успешно ажурирано {{count}} {{label}}.',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'Текст для отображения', textToDisplay: 'Текст для отображения',
}, },
general: { general: {
copy: 'Скопировать',
createdAt: 'Дата создания', createdAt: 'Дата создания',
deletedCountSuccessfully: 'Удалено {{count}} {{label}} успешно.', deletedCountSuccessfully: 'Удалено {{count}} {{label}} успешно.',
deletedSuccessfully: 'Удален успешно.', deletedSuccessfully: 'Удален успешно.',
email: 'Email', email: 'Email',
notFound: 'Не найдено', notFound: 'Не найдено',
successfullyCreated: '{{label}} успешно создан.', successfullyCreated: '{{label}} успешно создан.',
successfullyDuplicated: '{{label}} успешно продублирован.',
thisLanguage: 'Русский', thisLanguage: 'Русский',
updatedAt: 'Дата правки', updatedAt: 'Дата правки',
updatedCountSuccessfully: 'Обновлено {{count}} {{label}} успешно.', updatedCountSuccessfully: 'Обновлено {{count}} {{label}} успешно.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Text att visa', textToDisplay: 'Text att visa',
}, },
general: { general: {
copy: 'Kopiera',
createdAt: 'Skapad Vid', createdAt: 'Skapad Vid',
deletedCountSuccessfully: 'Raderade {{count}} {{label}} framgångsrikt.', deletedCountSuccessfully: 'Raderade {{count}} {{label}} framgångsrikt.',
deletedSuccessfully: 'Togs bort framgångsrikt.', deletedSuccessfully: 'Togs bort framgångsrikt.',
email: 'E-post', email: 'E-post',
notFound: 'Hittades inte', notFound: 'Hittades inte',
successfullyCreated: '{{label}} skapades framgångsrikt.', successfullyCreated: '{{label}} skapades framgångsrikt.',
successfullyDuplicated: '{{label}} duplicerades framgångsrikt.',
thisLanguage: 'Svenska', thisLanguage: 'Svenska',
updatedAt: 'Uppdaterades Vid', updatedAt: 'Uppdaterades Vid',
updatedCountSuccessfully: 'Uppdaterade {{count}} {{label}} framgångsrikt.', updatedCountSuccessfully: 'Uppdaterade {{count}} {{label}} framgångsrikt.',

View File

@@ -38,12 +38,14 @@ export default {
textToDisplay: 'ข้อความสำหรับแสดงผล', textToDisplay: 'ข้อความสำหรับแสดงผล',
}, },
general: { general: {
copy: 'คัดลอก',
createdAt: 'สร้างเมื่อ', createdAt: 'สร้างเมื่อ',
deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.', deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.',
deletedSuccessfully: 'ลบสำเร็จ', deletedSuccessfully: 'ลบสำเร็จ',
email: 'อีเมล', email: 'อีเมล',
notFound: 'ไม่พบ', notFound: 'ไม่พบ',
successfullyCreated: 'สร้าง {{label}} สำเร็จ', successfullyCreated: 'สร้าง {{label}} สำเร็จ',
successfullyDuplicated: 'สำเนา {{label}} สำเร็จ',
thisLanguage: 'ไทย', thisLanguage: 'ไทย',
updatedAt: 'แก้ไขเมื่อ', updatedAt: 'แก้ไขเมื่อ',
updatedCountSuccessfully: 'อัปเดต {{count}} {{label}} เรียบร้อยแล้ว', updatedCountSuccessfully: 'อัปเดต {{count}} {{label}} เรียบร้อยแล้ว',

View File

@@ -40,12 +40,14 @@ export default {
textToDisplay: 'Görüntülenecek metin', textToDisplay: 'Görüntülenecek metin',
}, },
general: { general: {
copy: 'Kopyala',
createdAt: 'Oluşturma tarihi', createdAt: 'Oluşturma tarihi',
deletedCountSuccessfully: '{{count}} {{label}} başarıyla silindi.', deletedCountSuccessfully: '{{count}} {{label}} başarıyla silindi.',
deletedSuccessfully: 'Başarıyla silindi.', deletedSuccessfully: 'Başarıyla silindi.',
email: 'E-posta', email: 'E-posta',
notFound: 'Bulunamadı', notFound: 'Bulunamadı',
successfullyCreated: '{{label}} başarıyla oluşturuldu.', successfullyCreated: '{{label}} başarıyla oluşturuldu.',
successfullyDuplicated: '{{label}} başarıyla kopyalandı.',
thisLanguage: 'Türkçe', thisLanguage: 'Türkçe',
updatedAt: 'Güncellenme tarihi', updatedAt: 'Güncellenme tarihi',
updatedCountSuccessfully: '{{count}} {{label}} başarıyla güncellendi.', updatedCountSuccessfully: '{{count}} {{label}} başarıyla güncellendi.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Текст для відображення', textToDisplay: 'Текст для відображення',
}, },
general: { general: {
copy: 'Скопіювати',
createdAt: 'Дата створення', createdAt: 'Дата створення',
deletedCountSuccessfully: 'Успішно видалено {{count}} {{label}}.', deletedCountSuccessfully: 'Успішно видалено {{count}} {{label}}.',
deletedSuccessfully: 'Успішно видалено.', deletedSuccessfully: 'Успішно видалено.',
email: 'Email', email: 'Email',
notFound: 'Не знайдено', notFound: 'Не знайдено',
successfullyCreated: '{{label}} успішно створено.', successfullyCreated: '{{label}} успішно створено.',
successfullyDuplicated: '{{label}} успішно продубльовано.',
thisLanguage: 'Українська', thisLanguage: 'Українська',
updatedAt: 'Змінено', updatedAt: 'Змінено',
updatedCountSuccessfully: 'Успішно оновлено {{count}} {{label}}.', updatedCountSuccessfully: 'Успішно оновлено {{count}} {{label}}.',

View File

@@ -39,12 +39,14 @@ export default {
textToDisplay: 'Văn bản để hiển thị', textToDisplay: 'Văn bản để hiển thị',
}, },
general: { general: {
copy: 'Sao chép',
createdAt: 'Ngày tạo', createdAt: 'Ngày tạo',
deletedCountSuccessfully: 'Đã xóa thành công {{count}} {{label}}.', deletedCountSuccessfully: 'Đã xóa thành công {{count}} {{label}}.',
deletedSuccessfully: 'Đã xoá thành công.', deletedSuccessfully: 'Đã xoá thành công.',
email: 'Email', email: 'Email',
notFound: 'Không tìm thấy', notFound: 'Không tìm thấy',
successfullyCreated: '{{label}} đã được tạo thành công.', successfullyCreated: '{{label}} đã được tạo thành công.',
successfullyDuplicated: '{{label}} đã được sao chép thành công.',
thisLanguage: 'Vietnamese (Tiếng Việt)', thisLanguage: 'Vietnamese (Tiếng Việt)',
updatedAt: 'Ngày cập nhật', updatedAt: 'Ngày cập nhật',
updatedCountSuccessfully: 'Đã cập nhật thành công {{count}} {{label}}.', updatedCountSuccessfully: 'Đã cập nhật thành công {{count}} {{label}}.',

View File

@@ -37,12 +37,14 @@ export default {
textToDisplay: '要顯示的文字', textToDisplay: '要顯示的文字',
}, },
general: { general: {
copy: '複製',
createdAt: '建立於', createdAt: '建立於',
deletedCountSuccessfully: '已成功刪除 {{count}} 個 {{label}}。', deletedCountSuccessfully: '已成功刪除 {{count}} 個 {{label}}。',
deletedSuccessfully: '已成功刪除。', deletedSuccessfully: '已成功刪除。',
email: '電子郵件', email: '電子郵件',
notFound: '未找到', notFound: '未找到',
successfullyCreated: '成功建立{{label}}', successfullyCreated: '成功建立{{label}}',
successfullyDuplicated: '成功複製{{label}}',
thisLanguage: '中文 (繁體)', thisLanguage: '中文 (繁體)',
updatedAt: '更新於', updatedAt: '更新於',
updatedCountSuccessfully: '已成功更新 {{count}} 個 {{label}}。', updatedCountSuccessfully: '已成功更新 {{count}} 個 {{label}}。',

View File

@@ -37,12 +37,14 @@ export default {
textToDisplay: '要显示的文本', textToDisplay: '要显示的文本',
}, },
general: { general: {
copy: '复制',
createdAt: '创建于', createdAt: '创建于',
deletedCountSuccessfully: '已成功删除 {{count}} {{label}}。', deletedCountSuccessfully: '已成功删除 {{count}} {{label}}。',
deletedSuccessfully: '已成功删除。', deletedSuccessfully: '已成功删除。',
email: '电子邮件', email: '电子邮件',
notFound: '未找到', notFound: '未找到',
successfullyCreated: '成功创建{{label}}', successfullyCreated: '成功创建{{label}}',
successfullyDuplicated: '成功复制{{label}}',
thisLanguage: '中文 (简体)', thisLanguage: '中文 (简体)',
updatedAt: '更新于', updatedAt: '更新于',
updatedCountSuccessfully: '已成功更新 {{count}} {{label}}。', updatedCountSuccessfully: '已成功更新 {{count}} {{label}}。',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'تمت الإرسال بنجاح.', submissionSuccessful: 'تمت الإرسال بنجاح.',
submit: 'إرسال', submit: 'إرسال',
successfullyCreated: '{{label}} تم إنشاؤها بنجاح.', successfullyCreated: '{{label}} تم إنشاؤها بنجاح.',
successfullyDuplicated: '{{label}} تم استنساخها بنجاح.',
thisLanguage: 'العربية', thisLanguage: 'العربية',
titleDeleted: 'تم حذف {{label}} "{{title}}" بنجاح.', titleDeleted: 'تم حذف {{label}} "{{title}}" بنجاح.',
unauthorized: 'غير مصرح به', unauthorized: 'غير مصرح به',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Təqdimat uğurlu oldu.', submissionSuccessful: 'Təqdimat uğurlu oldu.',
submit: 'Təqdim et', submit: 'Təqdim et',
successfullyCreated: '{{label}} uğurla yaradıldı.', successfullyCreated: '{{label}} uğurla yaradıldı.',
successfullyDuplicated: '{{label}} uğurla dublikatlandı.',
thisLanguage: 'Azərbaycan dili', thisLanguage: 'Azərbaycan dili',
titleDeleted: '{{label}} "{{title}}" uğurla silindi.', titleDeleted: '{{label}} "{{title}}" uğurla silindi.',
unauthorized: 'İcazəsiz', unauthorized: 'İcazəsiz',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'Успешно подаване.', submissionSuccessful: 'Успешно подаване.',
submit: 'Подай', submit: 'Подай',
successfullyCreated: '{{label}} успешно създаден.', successfullyCreated: '{{label}} успешно създаден.',
successfullyDuplicated: '{{label}} успешно дупликиран.',
thisLanguage: 'Български', thisLanguage: 'Български',
titleDeleted: '{{label}} "{{title}}" успешно изтрит.', titleDeleted: '{{label}} "{{title}}" успешно изтрит.',
unauthorized: 'Неавторизиран', unauthorized: 'Неавторизиран',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'Odeslání úspěšné.', submissionSuccessful: 'Odeslání úspěšné.',
submit: 'Odeslat', submit: 'Odeslat',
successfullyCreated: '{{label}} úspěšně vytvořeno.', successfullyCreated: '{{label}} úspěšně vytvořeno.',
successfullyDuplicated: '{{label}} úspěšně duplikováno.',
thisLanguage: 'Čeština', thisLanguage: 'Čeština',
titleDeleted: '{{label}} "{{title}}" úspěšně smazáno.', titleDeleted: '{{label}} "{{title}}" úspěšně smazáno.',
unauthorized: 'Neoprávněný', unauthorized: 'Neoprávněný',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Einrichung erfolgreich.', submissionSuccessful: 'Einrichung erfolgreich.',
submit: 'Senden', submit: 'Senden',
successfullyCreated: '{{label}} erfolgreich erstellt.', successfullyCreated: '{{label}} erfolgreich erstellt.',
successfullyDuplicated: '{{label}} wurde erfolgreich dupliziert.',
thisLanguage: 'Deutsch', thisLanguage: 'Deutsch',
titleDeleted: '{{label}} {{title}} wurde erfolgreich gelöscht.', titleDeleted: '{{label}} {{title}} wurde erfolgreich gelöscht.',
unauthorized: 'Nicht autorisiert', unauthorized: 'Nicht autorisiert',

View File

@@ -47,8 +47,6 @@ export default {
deletingTitle: deletingTitle:
'There was an error while deleting {{title}}. Please check your connection and try again.', 'There was an error while deleting {{title}}. Please check your connection and try again.',
loadingDocument: 'There was a problem loading the document with ID of {{id}}.', loadingDocument: 'There was a problem loading the document with ID of {{id}}.',
localesNotSaved_one: 'The following locale could not be saved:',
localesNotSaved_other: 'The following locales could not be saved:',
noMatchedField: 'No matched field found for "{{label}}"', noMatchedField: 'No matched field found for "{{label}}"',
notAllowedToAccessPage: 'You are not allowed to access this page.', notAllowedToAccessPage: 'You are not allowed to access this page.',
previewing: 'There was a problem previewing this document.', previewing: 'There was a problem previewing this document.',
@@ -191,7 +189,6 @@ export default {
submissionSuccessful: 'Submission Successful.', submissionSuccessful: 'Submission Successful.',
submit: 'Submit', submit: 'Submit',
successfullyCreated: '{{label}} successfully created.', successfullyCreated: '{{label}} successfully created.',
successfullyDuplicated: '{{label}} successfully duplicated.',
thisLanguage: 'English', thisLanguage: 'English',
titleDeleted: '{{label}} "{{title}}" successfully deleted.', titleDeleted: '{{label}} "{{title}}" successfully deleted.',
unauthorized: 'Unauthorized', unauthorized: 'Unauthorized',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Envío realizado correctamente.', submissionSuccessful: 'Envío realizado correctamente.',
submit: 'Enviar', submit: 'Enviar',
successfullyCreated: '{{label}} creado correctamente.', successfullyCreated: '{{label}} creado correctamente.',
successfullyDuplicated: '{{label}} duplicado correctamente.',
thisLanguage: 'Español', thisLanguage: 'Español',
titleDeleted: '{{label}} {{title}} eliminado correctamente.', titleDeleted: '{{label}} {{title}} eliminado correctamente.',
unauthorized: 'No autorizado', unauthorized: 'No autorizado',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'با موفقیت ثبت شد.', submissionSuccessful: 'با موفقیت ثبت شد.',
submit: 'فرستادن', submit: 'فرستادن',
successfullyCreated: '{{label}} با موفقیت ساخته شد.', successfullyCreated: '{{label}} با موفقیت ساخته شد.',
successfullyDuplicated: '{{label}} با موفقیت رونوشت شد.',
thisLanguage: 'فارسی', thisLanguage: 'فارسی',
titleDeleted: '{{label}} "{{title}}" با موفقیت پاک شد.', titleDeleted: '{{label}} "{{title}}" با موفقیت پاک شد.',
unauthorized: 'غیرمجاز', unauthorized: 'غیرمجاز',

View File

@@ -191,7 +191,6 @@ export default {
submissionSuccessful: 'Soumission réussie.', submissionSuccessful: 'Soumission réussie.',
submit: 'Soumettre', submit: 'Soumettre',
successfullyCreated: '{{label}} créé(e) avec succès.', successfullyCreated: '{{label}} créé(e) avec succès.',
successfullyDuplicated: '{{label}} dupliqué(e) avec succès.',
thisLanguage: 'Français', thisLanguage: 'Français',
titleDeleted: '{{label}} "{{title}}" supprimé(e) avec succès.', titleDeleted: '{{label}} "{{title}}" supprimé(e) avec succès.',
unauthorized: 'Non autorisé', unauthorized: 'Non autorisé',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'Uspješno slanje', submissionSuccessful: 'Uspješno slanje',
submit: 'Podnesi', submit: 'Podnesi',
successfullyCreated: '{{label}} uspješno kreirano.', successfullyCreated: '{{label}} uspješno kreirano.',
successfullyDuplicated: '{{label}} uspješno duplicirano.',
thisLanguage: 'Hrvatski', thisLanguage: 'Hrvatski',
titleDeleted: '{{label}} "{{title}}" uspješno obrisano.', titleDeleted: '{{label}} "{{title}}" uspješno obrisano.',
unauthorized: 'Neovlašteno', unauthorized: 'Neovlašteno',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Beküldés sikeres.', submissionSuccessful: 'Beküldés sikeres.',
submit: 'Beküldés', submit: 'Beküldés',
successfullyCreated: '{{label}} sikeresen létrehozva.', successfullyCreated: '{{label}} sikeresen létrehozva.',
successfullyDuplicated: '{{label}} sikeresen duplikálódott.',
thisLanguage: 'Magyar', thisLanguage: 'Magyar',
titleDeleted: '{{label}} "{{title}}" sikeresen törölve.', titleDeleted: '{{label}} "{{title}}" sikeresen törölve.',
unauthorized: 'Jogosulatlan', unauthorized: 'Jogosulatlan',

View File

@@ -190,7 +190,6 @@ export default {
submissionSuccessful: 'Invio riuscito.', submissionSuccessful: 'Invio riuscito.',
submit: 'Invia', submit: 'Invia',
successfullyCreated: '{{label}} creato con successo.', successfullyCreated: '{{label}} creato con successo.',
successfullyDuplicated: '{{label}} duplicato con successo.',
thisLanguage: 'Italiano', thisLanguage: 'Italiano',
titleDeleted: '{{label}} {{title}} eliminato con successo.', titleDeleted: '{{label}} {{title}} eliminato con successo.',
unauthorized: 'Non autorizzato', unauthorized: 'Non autorizzato',

View File

@@ -47,8 +47,6 @@ export default {
deletingTitle: deletingTitle:
'{{title}} を削除する際にエラーが発生しました。接続を確認してからもう一度お試しください。', '{{title}} を削除する際にエラーが発生しました。接続を確認してからもう一度お試しください。',
loadingDocument: 'IDが {{id}} のデータを読み込む際に問題が発生しました。', loadingDocument: 'IDが {{id}} のデータを読み込む際に問題が発生しました。',
localesNotSaved_one: '次のロケールは保存できませんでした:',
localesNotSaved_other: '次のロケールは保存できませんでした:',
noMatchedField: '"{{label}}" に該当するフィールドがありません。', noMatchedField: '"{{label}}" に該当するフィールドがありません。',
notAllowedToAccessPage: 'この画面へのアクセスは許可されていません。', notAllowedToAccessPage: 'この画面へのアクセスは許可されていません。',
previewing: 'このデータをプレビューする際に問題が発生しました。', previewing: 'このデータをプレビューする際に問題が発生しました。',
@@ -190,7 +188,6 @@ export default {
submissionSuccessful: '送信が成功しました。', submissionSuccessful: '送信が成功しました。',
submit: '送信', submit: '送信',
successfullyCreated: '{{label}} が作成されました。', successfullyCreated: '{{label}} が作成されました。',
successfullyDuplicated: '{{label}} が複製されました。',
thisLanguage: 'Japanese', thisLanguage: 'Japanese',
titleDeleted: '{{label}} "{{title}}" が削除されました。', titleDeleted: '{{label}} "{{title}}" が削除されました。',
unauthorized: '未認証', unauthorized: '未認証',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: '제출이 완료되었습니다.', submissionSuccessful: '제출이 완료되었습니다.',
submit: '제출', submit: '제출',
successfullyCreated: '{{label}}이(가) 생성되었습니다.', successfullyCreated: '{{label}}이(가) 생성되었습니다.',
successfullyDuplicated: '{{label}}이(가) 복제되었습니다.',
thisLanguage: '한국어', thisLanguage: '한국어',
titleDeleted: '{{label}} "{{title}}"을(를) 삭제했습니다.', titleDeleted: '{{label}} "{{title}}"을(를) 삭제했습니다.',
unauthorized: '권한 없음', unauthorized: '권한 없음',

View File

@@ -190,7 +190,6 @@ export default {
submissionSuccessful: 'သိမ်းဆည်းမှု အောင်မြင်ပါသည်။', submissionSuccessful: 'သိမ်းဆည်းမှု အောင်မြင်ပါသည်။',
submit: 'သိမ်းဆည်းမည်။', submit: 'သိမ်းဆည်းမည်။',
successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။', successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။',
successfullyDuplicated: '{{label}} အောင်မြင်စွာ ပုံတူပွားခဲ့သည်။',
thisLanguage: 'မြန်မာစာ', thisLanguage: 'မြန်မာစာ',
titleDeleted: '{{label}} {{title}} အောင်မြင်စွာ ဖျက်သိမ်းခဲ့သည်။', titleDeleted: '{{label}} {{title}} အောင်မြင်စွာ ဖျက်သိမ်းခဲ့သည်။',
unauthorized: 'အခွင့်မရှိပါ။', unauthorized: 'အခွင့်မရှိပါ။',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Innsending vellykket.', submissionSuccessful: 'Innsending vellykket.',
submit: 'Send inn', submit: 'Send inn',
successfullyCreated: '{{label}} ble opprettet.', successfullyCreated: '{{label}} ble opprettet.',
successfullyDuplicated: '{{label}} ble duplisert.',
thisLanguage: 'Norsk', thisLanguage: 'Norsk',
titleDeleted: '{{label}} "{{title}}" ble slettet.', titleDeleted: '{{label}} "{{title}}" ble slettet.',
unauthorized: 'Ikke autorisert', unauthorized: 'Ikke autorisert',

View File

@@ -190,7 +190,6 @@ export default {
submissionSuccessful: 'Indiening succesvol.', submissionSuccessful: 'Indiening succesvol.',
submit: 'Indienen', submit: 'Indienen',
successfullyCreated: '{{label}} succesvol aangemaakt.', successfullyCreated: '{{label}} succesvol aangemaakt.',
successfullyDuplicated: '{{label}} succesvol gedupliceerd.',
thisLanguage: 'Nederlands', thisLanguage: 'Nederlands',
titleDeleted: '{{label}} "{{title}}" succesvol verwijderd.', titleDeleted: '{{label}} "{{title}}" succesvol verwijderd.',
unauthorized: 'Onbevoegd', unauthorized: 'Onbevoegd',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Zgłoszenie zakończone powodzeniem.', submissionSuccessful: 'Zgłoszenie zakończone powodzeniem.',
submit: 'Zatwierdź', submit: 'Zatwierdź',
successfullyCreated: 'Pomyślnie utworzono {{label}}.', successfullyCreated: 'Pomyślnie utworzono {{label}}.',
successfullyDuplicated: 'Pomyślnie zduplikowano {{label}}',
thisLanguage: 'Polski', thisLanguage: 'Polski',
titleDeleted: 'Pomyślnie usunięto {{label}} {{title}}', titleDeleted: 'Pomyślnie usunięto {{label}} {{title}}',
unauthorized: 'Brak autoryzacji', unauthorized: 'Brak autoryzacji',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Envio bem-sucedido.', submissionSuccessful: 'Envio bem-sucedido.',
submit: 'Enviar', submit: 'Enviar',
successfullyCreated: '{{label}} criado com sucesso.', successfullyCreated: '{{label}} criado com sucesso.',
successfullyDuplicated: '{{label}} duplicado com sucesso.',
thisLanguage: 'Português', thisLanguage: 'Português',
titleDeleted: '{{label}} {{title}} excluído com sucesso.', titleDeleted: '{{label}} {{title}} excluído com sucesso.',
unauthorized: 'Não autorizado', unauthorized: 'Não autorizado',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Trimitere cu succes.', submissionSuccessful: 'Trimitere cu succes.',
submit: 'Trimite', submit: 'Trimite',
successfullyCreated: '{{label}} creat(ă) cu succes.', successfullyCreated: '{{label}} creat(ă) cu succes.',
successfullyDuplicated: '{{label}} duplicat(ă) cu succes.',
thisLanguage: 'Română', thisLanguage: 'Română',
titleDeleted: '{{label}} "{{title}}" șters cu succes.', titleDeleted: '{{label}} "{{title}}" șters cu succes.',
unauthorized: 'neautorizat(ă)', unauthorized: 'neautorizat(ă)',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'Uspešno slanje', submissionSuccessful: 'Uspešno slanje',
submit: 'Potvrdi', submit: 'Potvrdi',
successfullyCreated: '{{label}} uspešno kreirano.', successfullyCreated: '{{label}} uspešno kreirano.',
successfullyDuplicated: '{{label}} uspešno duplicirano.',
thisLanguage: 'Srpski (latinica)', thisLanguage: 'Srpski (latinica)',
titleDeleted: '{{label}} "{{title}}" uspešno obrisano.', titleDeleted: '{{label}} "{{title}}" uspešno obrisano.',
unauthorized: 'Niste autorizovani', unauthorized: 'Niste autorizovani',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'Успешно слање', submissionSuccessful: 'Успешно слање',
submit: 'Потврди', submit: 'Потврди',
successfullyCreated: '{{label}} успешно креирано.', successfullyCreated: '{{label}} успешно креирано.',
successfullyDuplicated: '{{label}} успешно дуплицирано.',
thisLanguage: 'Српски (ћирилица)', thisLanguage: 'Српски (ћирилица)',
titleDeleted: '{{label}} "{{title}}" успешно обрисано.', titleDeleted: '{{label}} "{{title}}" успешно обрисано.',
unauthorized: 'Нисте ауторизовани', unauthorized: 'Нисте ауторизовани',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Успешно отправлено.', submissionSuccessful: 'Успешно отправлено.',
submit: 'Отправить', submit: 'Отправить',
successfullyCreated: '{{label}} успешно создан.', successfullyCreated: '{{label}} успешно создан.',
successfullyDuplicated: '{{label}} успешно продублирован.',
thisLanguage: 'Русский', thisLanguage: 'Русский',
titleDeleted: '{{label}} {{title}} успешно удалено.', titleDeleted: '{{label}} {{title}} успешно удалено.',
unauthorized: 'Нет доступа', unauthorized: 'Нет доступа',

View File

@@ -189,7 +189,6 @@ export default {
submissionSuccessful: 'Inlämningen Lyckades.', submissionSuccessful: 'Inlämningen Lyckades.',
submit: 'Lämna in', submit: 'Lämna in',
successfullyCreated: '{{label}} skapades framgångsrikt.', successfullyCreated: '{{label}} skapades framgångsrikt.',
successfullyDuplicated: '{{label}} duplicerades framgångsrikt.',
thisLanguage: 'Svenska', thisLanguage: 'Svenska',
titleDeleted: '{{label}} "{{title}}" togs bort framgångsrikt.', titleDeleted: '{{label}} "{{title}}" togs bort framgångsrikt.',
unauthorized: 'Obehörig', unauthorized: 'Obehörig',

View File

@@ -187,7 +187,6 @@ export default {
submissionSuccessful: 'ส่งสำเร็จ', submissionSuccessful: 'ส่งสำเร็จ',
submit: 'ส่ง', submit: 'ส่ง',
successfullyCreated: 'สร้าง {{label}} สำเร็จ', successfullyCreated: 'สร้าง {{label}} สำเร็จ',
successfullyDuplicated: 'สำเนา {{label}} สำเร็จ',
thisLanguage: 'ไทย', thisLanguage: 'ไทย',
titleDeleted: 'ลบ {{label}} "{{title}}" สำเร็จ', titleDeleted: 'ลบ {{label}} "{{title}}" สำเร็จ',
unauthorized: 'ไม่ได้รับอนุญาต', unauthorized: 'ไม่ได้รับอนุญาต',

View File

@@ -190,7 +190,6 @@ export default {
submissionSuccessful: 'Gönderme başarılı', submissionSuccessful: 'Gönderme başarılı',
submit: 'Gönder', submit: 'Gönder',
successfullyCreated: '{{label}} başarıyla oluşturuldu.', successfullyCreated: '{{label}} başarıyla oluşturuldu.',
successfullyDuplicated: '{{label}} başarıyla kopyalandı.',
thisLanguage: 'Türkçe', thisLanguage: 'Türkçe',
titleDeleted: '{{label}} {{title}} başarıyla silindi.', titleDeleted: '{{label}} {{title}} başarıyla silindi.',
unauthorized: 'Yetkisiz', unauthorized: 'Yetkisiz',

View File

@@ -188,7 +188,6 @@ export default {
submissionSuccessful: 'Успішно відправлено.', submissionSuccessful: 'Успішно відправлено.',
submit: 'Відправити', submit: 'Відправити',
successfullyCreated: '{{label}} успішно створено.', successfullyCreated: '{{label}} успішно створено.',
successfullyDuplicated: '{{label}} успішно продубльовано.',
thisLanguage: 'Українська', thisLanguage: 'Українська',
titleDeleted: '{{label}} "{{title}}" успішно видалено.', titleDeleted: '{{label}} "{{title}}" успішно видалено.',
unauthorized: 'Немає доступу', unauthorized: 'Немає доступу',

View File

@@ -187,7 +187,6 @@ export default {
submissionSuccessful: 'Gửi thành công.', submissionSuccessful: 'Gửi thành công.',
submit: 'Gửi', submit: 'Gửi',
successfullyCreated: '{{label}} đã được tạo thành công.', successfullyCreated: '{{label}} đã được tạo thành công.',
successfullyDuplicated: '{{label}} đã được sao chép thành công.',
thisLanguage: 'Vietnamese (Tiếng Việt)', thisLanguage: 'Vietnamese (Tiếng Việt)',
titleDeleted: '{{label}} {{title}} đã được xóa thành công.', titleDeleted: '{{label}} {{title}} đã được xóa thành công.',
unauthorized: 'Không có quyền truy cập.', unauthorized: 'Không có quyền truy cập.',

View File

@@ -46,8 +46,6 @@ export default {
correctInvalidFields: '請更正無效區塊。', correctInvalidFields: '請更正無效區塊。',
deletingTitle: '刪除{{title}}時出現了錯誤。請檢查您的網路連線並重試。', deletingTitle: '刪除{{title}}時出現了錯誤。請檢查您的網路連線並重試。',
loadingDocument: '加載ID為{{id}}的文件時出現了問題。', loadingDocument: '加載ID為{{id}}的文件時出現了問題。',
localesNotSaved_one: '這個語言環境無法被儲存:',
localesNotSaved_other: '以下的語言環境無法被儲存:',
noMatchedField: '找不到與"{{label}}"匹配的字串', noMatchedField: '找不到與"{{label}}"匹配的字串',
notAllowedToAccessPage: '您沒有權限訪問此頁面。', notAllowedToAccessPage: '您沒有權限訪問此頁面。',
previewing: '預覽文件時出現了問題。', previewing: '預覽文件時出現了問題。',
@@ -188,7 +186,6 @@ export default {
submissionSuccessful: '成功送出。', submissionSuccessful: '成功送出。',
submit: '送出', submit: '送出',
successfullyCreated: '成功建立{{label}}', successfullyCreated: '成功建立{{label}}',
successfullyDuplicated: '成功複製{{label}}',
thisLanguage: '中文 (繁體)', thisLanguage: '中文 (繁體)',
titleDeleted: '{{label}} "{{title}}"已被成功刪除。', titleDeleted: '{{label}} "{{title}}"已被成功刪除。',
unauthorized: '未經授權', unauthorized: '未經授權',

View File

@@ -186,7 +186,6 @@ export default {
submissionSuccessful: '提交成功。', submissionSuccessful: '提交成功。',
submit: '提交', submit: '提交',
successfullyCreated: '成功创建{{label}}', successfullyCreated: '成功创建{{label}}',
successfullyDuplicated: '成功复制{{label}}',
thisLanguage: '中文 (简体)', thisLanguage: '中文 (简体)',
titleDeleted: '{{label}} "{{title}}"已被成功删除。', titleDeleted: '{{label}} "{{title}}"已被成功删除。',
unauthorized: '未经授权', unauthorized: '未经授权',

View File

@@ -77,8 +77,6 @@ export default {
invalidFileType: 'Invalid file type', invalidFileType: 'Invalid file type',
invalidFileTypeValue: 'Invalid file type: {{value}}', invalidFileTypeValue: 'Invalid file type: {{value}}',
loadingDocument: 'There was a problem loading the document with ID of {{id}}.', loadingDocument: 'There was a problem loading the document with ID of {{id}}.',
localesNotSaved_one: 'The following locale could not be saved:',
localesNotSaved_other: 'The following locales could not be saved:',
missingEmail: 'Missing email.', missingEmail: 'Missing email.',
missingIDOfDocument: 'Missing ID of document to update.', missingIDOfDocument: 'Missing ID of document to update.',
missingIDOfVersion: 'Missing ID of version.', missingIDOfVersion: 'Missing ID of version.',

View File

@@ -78,8 +78,6 @@ export default {
invalidFileType: '無効なファイル形式', invalidFileType: '無効なファイル形式',
invalidFileTypeValue: '無効なファイル形式: {{value}}', invalidFileTypeValue: '無効なファイル形式: {{value}}',
loadingDocument: 'IDが {{id}} のデータを読み込む際に問題が発生しました。', loadingDocument: 'IDが {{id}} のデータを読み込む際に問題が発生しました。',
localesNotSaved_one: '次のロケールは保存できませんでした:',
localesNotSaved_other: '次のロケールは保存できませんでした:',
missingEmail: 'メールアドレスが不足しています。', missingEmail: 'メールアドレスが不足しています。',
missingIDOfDocument: '更新するデータのIDが不足しています。', missingIDOfDocument: '更新するデータのIDが不足しています。',
missingIDOfVersion: 'バージョンIDが不足しています。', missingIDOfVersion: 'バージョンIDが不足しています。',

View File

@@ -72,8 +72,6 @@ export default {
invalidFileType: '無效的文件類型', invalidFileType: '無效的文件類型',
invalidFileTypeValue: '無效的文件類型: {{value}}', invalidFileTypeValue: '無效的文件類型: {{value}}',
loadingDocument: '加載ID為{{id}}的文件時出現了問題。', loadingDocument: '加載ID為{{id}}的文件時出現了問題。',
localesNotSaved_one: '這個語言環境無法被儲存:',
localesNotSaved_other: '以下的語言環境無法被儲存:',
missingEmail: '缺少電子郵件。', missingEmail: '缺少電子郵件。',
missingIDOfDocument: '缺少需要更新的文檔的ID。', missingIDOfDocument: '缺少需要更新的文檔的ID。',
missingIDOfVersion: '缺少版本的ID。', missingIDOfVersion: '缺少版本的ID。',

View File

@@ -30,12 +30,14 @@ const serverTranslationKeys = [
'fields:chooseDocumentToLink', 'fields:chooseDocumentToLink',
'fields:openInNewTab', 'fields:openInNewTab',
'general:copy',
'general:createdAt', 'general:createdAt',
'general:deletedCountSuccessfully', 'general:deletedCountSuccessfully',
'general:deletedSuccessfully', 'general:deletedSuccessfully',
'general:email', 'general:email',
'general:notFound', 'general:notFound',
'general:successfullyCreated', 'general:successfullyCreated',
'general:successfullyDuplicated',
'general:thisLanguage', 'general:thisLanguage',
'general:user', 'general:user',
'general:users', 'general:users',
@@ -130,7 +132,6 @@ const clientTranslationKeys = [
'error:correctInvalidFields', 'error:correctInvalidFields',
'error:deletingTitle', 'error:deletingTitle',
'error:loadingDocument', 'error:loadingDocument',
'error:localesNotSaved',
'error:noMatchedField', 'error:noMatchedField',
'error:notAllowedToAccessPage', 'error:notAllowedToAccessPage',
'error:previewing', 'error:previewing',
@@ -266,7 +267,6 @@ const clientTranslationKeys = [
'general:submit', 'general:submit',
'general:successfullyCreated', 'general:successfullyCreated',
'general:successfullyDeleted', 'general:successfullyDeleted',
'general:successfullyDuplicated',
'general:thisLanguage', 'general:thisLanguage',
'general:titleDeleted', 'general:titleDeleted',
'general:unauthorized', 'general:unauthorized',

View File

@@ -7,11 +7,11 @@ import { toast } from 'react-toastify'
import type { Props } from './types.js' import type { Props } from './types.js'
// import { requests } from '../../../api'
import { useForm, useFormModified } from '../../forms/Form/context.js' import { useForm, useFormModified } from '../../forms/Form/context.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { MinimalTemplate } from '../../templates/Minimal/index.js' import { MinimalTemplate } from '../../templates/Minimal/index.js'
import { requests } from '../../utilities/api.js'
import { Button } from '../Button/index.js' import { Button } from '../Button/index.js'
import * as PopupList from '../Popup/PopupButtonList/index.js' import * as PopupList from '../Popup/PopupButtonList/index.js'
import './index.scss' import './index.scss'
@@ -21,12 +21,11 @@ const baseClass = 'duplicate'
const Duplicate: React.FC<Props> = ({ id, slug, singularLabel }) => { const Duplicate: React.FC<Props> = ({ id, slug, singularLabel }) => {
const { Modal, useModal } = facelessUIImport const { Modal, useModal } = facelessUIImport
const { push } = useRouter() const router = useRouter()
const modified = useFormModified() const modified = useFormModified()
const { toggleModal } = useModal() const { toggleModal } = useModal()
const { setModified } = useForm() const { setModified } = useForm()
const { const {
localization,
routes: { api }, routes: { api },
serverURL, serverURL,
} = useConfig() } = useConfig()
@@ -46,134 +45,35 @@ const Duplicate: React.FC<Props> = ({ id, slug, singularLabel }) => {
toggleModal(modalSlug) toggleModal(modalSlug)
return return
} }
await requests.post(`${serverURL}${api}/${slug}/${id}/duplicate`, {
const saveDocument = async ({ body: JSON.stringify({}),
id, headers: {
duplicateID = '', 'Accept-Language': i18n.language,
locale = '', 'Content-Type': 'application/json',
}): Promise<null | string> => { 'credentials': 'include',
const data = null
// const response = await requests.get(`${serverURL}${api}/${slug}/${id}`, {
// headers: {
// 'Accept-Language': i18n.language,
// },
// params: {
// depth: 0,
// draft: true,
// 'fallback-locale': 'none',
// locale,
// },
// })
// let data = await response.json()
// TODO: convert this into a server action
// if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
// data = await collection.admin.hooks.beforeDuplicate({
// collection,
// data,
// locale,
// })
// }
if (!duplicateID) {
if ('createdAt' in data) delete data.createdAt
if ('updatedAt' in data) delete data.updatedAt
}
// const result = await requests[duplicateID ? 'patch' : 'post'](
// `${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}&fallback-locale=none`,
// {
// body: JSON.stringify(data),
// headers: {
// 'Accept-Language': i18n.language,
// 'Content-Type': 'application/json',
// },
// },
// )
// const json = await result.json()
// if (result.status === 201 || result.status === 200) {
// return json.doc.id
// }
// only show the error if this is the initial request failing
if (!duplicateID) {
// json.errors.forEach((error) => toast.error(error.message))
}
return null
}
let duplicateID: string
let abort = false
const localeErrors = []
if (localization) {
await localization.localeCodes.reduce(async (priorLocalePatch, locale) => {
await priorLocalePatch
if (abort) return
const localeResult = await saveDocument({
id,
duplicateID,
locale,
})
duplicateID = localeResult || duplicateID
if (duplicateID && !localeResult) {
localeErrors.push(locale)
}
if (!duplicateID) {
abort = true
}
}, Promise.resolve())
} else {
duplicateID = await saveDocument({ id })
}
if (!duplicateID) {
// document was not saved, error toast was displayed
return
}
toast.success(
t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }),
{
autoClose: 3000,
}, },
) }).then(async (res) => {
const { doc, message } = await res.json()
if (localeErrors.length > 0) { if (res.status < 400) {
toast.error( toast.success(
` message ||
${t('error:localesNotSaved', { count: localeErrors.length })} t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }),
${localeErrors.join(', ')} {
`, autoClose: 3000,
{ autoClose: 5000 }, },
) )
} setModified(false)
router.push(`${admin}/collections/${slug}/${doc.id}`)
setModified(false) } else {
toast.error(
setTimeout(() => { message ||
push(`${admin}/collections/${slug}/${duplicateID}`) t('error:unspecific', { label: getTranslation(singularLabel, i18n) }),
}, 10) { autoClose: 5000 },
)
}
})
}, },
[ [modified, serverURL, api, slug, id, i18n, toggleModal, modalSlug, t, singularLabel, setModified, router, admin],
modified,
localization,
t,
i18n,
setModified,
toggleModal,
modalSlug,
serverURL,
api,
slug,
id,
push,
admin,
],
) )
const confirm = useCallback(async () => { const confirm = useCallback(async () => {

View File

@@ -1,31 +1,10 @@
import type { BeforeDuplicate, CollectionConfig } from 'payload/types' import type {
import type { IndexedField } from '../../payload-types.js' CollectionConfig} from 'payload/types'
import { indexedFieldsSlug } from '../../slugs.js' import { indexedFieldsSlug } from '../../slugs.js'
const beforeDuplicate: BeforeDuplicate<IndexedField> = ({ data }) => {
return {
...data,
collapsibleLocalizedUnique: data.collapsibleLocalizedUnique
? `${data.collapsibleLocalizedUnique}-copy`
: '',
collapsibleTextUnique: data.collapsibleTextUnique ? `${data.collapsibleTextUnique}-copy` : '',
group: {
...(data.group || {}),
localizedUnique: data.group?.localizedUnique ? `${data.group?.localizedUnique}-copy` : '',
},
uniqueText: data.uniqueText ? `${data.uniqueText}-copy` : '',
}
}
const IndexedFields: CollectionConfig = { const IndexedFields: CollectionConfig = {
slug: indexedFieldsSlug, slug: indexedFieldsSlug,
// used to assert that versions also get indexes
admin: {
hooks: {
beforeDuplicate,
},
},
fields: [ fields: [
{ {
name: 'text', name: 'text',

View File

@@ -619,6 +619,23 @@ describe('Fields', () => {
expect(result.id).toBeDefined() expect(result.id).toBeDefined()
}) })
it('should duplicate with unique fields', async () => {
const data = {
text: 'a',
}
const doc = await payload.create({
collection: 'indexed-fields',
data,
})
const result = await payload.duplicate({
collection: 'indexed-fields',
id: doc.id,
})
expect(result.id).not.toEqual(doc.id)
expect(result.uniqueRequiredText).toStrictEqual('uniqueRequired - Copy')
})
}) })
describe('array', () => { describe('array', () => {