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:
@@ -167,53 +167,6 @@ those three fields plus the ID field.
|
||||
so your admin queries can remain performant.
|
||||
</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
|
||||
|
||||
You can import collection types as follows:
|
||||
|
||||
@@ -21,6 +21,7 @@ functionalities to be easily reusable across your projects.
|
||||
|
||||
- [beforeValidate](#beforevalidate)
|
||||
- [beforeChange](#beforechange)
|
||||
- beforeDuplicate(#beforeduplicate)
|
||||
- [afterChange](#afterchange)
|
||||
- [afterRead](#afterread)
|
||||
|
||||
@@ -38,6 +39,7 @@ const ExampleField: Field = {
|
||||
hooks: {
|
||||
beforeValidate: [(args) => {...}],
|
||||
beforeChange: [(args) => {...}],
|
||||
beforeDuplicate: [(args) => {...}],
|
||||
afterChange: [(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
|
||||
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
|
||||
|
||||
Payload exports a type for field hooks which can be accessed and used as follows:
|
||||
|
||||
45
packages/graphql/src/resolvers/collections/duplicate.ts
Normal file
45
packages/graphql/src/resolvers/collections/duplicate.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import verifyEmail from '../resolvers/auth/verifyEmail.js'
|
||||
import createResolver from '../resolvers/collections/create.js'
|
||||
import getDeleteResolver from '../resolvers/collections/delete.js'
|
||||
import { docAccessResolver } from '../resolvers/collections/docAccess.js'
|
||||
import duplicateResolver from '../resolvers/collections/duplicate.js'
|
||||
import findResolver from '../resolvers/collections/find.js'
|
||||
import findByIDResolver from '../resolvers/collections/findByID.js'
|
||||
import findVersionByIDResolver from '../resolvers/collections/findVersionByID.js'
|
||||
@@ -237,6 +238,14 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
resolve: getDeleteResolver(collection),
|
||||
}
|
||||
|
||||
graphqlResult.Mutation.fields[`duplicate${singularName}`] = {
|
||||
type: collection.graphQL.type,
|
||||
args: {
|
||||
id: { type: new GraphQLNonNull(idType) },
|
||||
},
|
||||
resolve: duplicateResolver(collection),
|
||||
}
|
||||
|
||||
if (collectionConfig.versions) {
|
||||
const versionIDType = config.db.defaultIDType === 'text' ? GraphQLString : GraphQLInt
|
||||
const versionCollectionFields: Field[] = [
|
||||
|
||||
33
packages/next/src/routes/rest/collections/duplicate.ts
Normal file
33
packages/next/src/routes/rest/collections/duplicate.ts
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import { findVersionByID as findVersionByIdGlobal } from './globals/findVersionB
|
||||
import { findVersions as findVersionsGlobal } from './globals/findVersions.js'
|
||||
import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js'
|
||||
import { update as updateGlobal } from './globals/update.js'
|
||||
import { duplicate } from './collections/duplicate.js'
|
||||
|
||||
const endpoints = {
|
||||
collection: {
|
||||
@@ -71,6 +72,7 @@ const endpoints = {
|
||||
'doc-access-by-id': docAccess,
|
||||
'doc-verify-by-id': verifyEmail,
|
||||
'doc-versions-by-id': restoreVersion,
|
||||
duplicate,
|
||||
'first-register': registerFirstUser,
|
||||
'forgot-password': forgotPassword,
|
||||
login,
|
||||
@@ -356,6 +358,9 @@ export const POST =
|
||||
res = await (
|
||||
endpoints.collection.POST[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
|
||||
)({ id: slug3, collection, req })
|
||||
} else if (slug3 === 'duplicate') {
|
||||
// /:collection/:id/duplicate
|
||||
res = await endpoints.collection.POST.duplicate({ id: slug2, collection, req })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ const sanitizeCollection = (
|
||||
if (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.admin.useAsTitle =
|
||||
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) {
|
||||
sanitized.auth.strategies = []
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import joi from 'joi'
|
||||
|
||||
import { endpointsSchema } from '../../config/schema.js'
|
||||
import {
|
||||
componentSchema,
|
||||
customViewSchema,
|
||||
livePreviewSchema,
|
||||
} from '../../config/shared/componentSchema.js'
|
||||
import { componentSchema, customViewSchema, livePreviewSchema, } from '../../config/shared/componentSchema.js'
|
||||
|
||||
const strategyBaseSchema = joi.object().keys({
|
||||
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()])),
|
||||
hidden: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
hideAPIURL: joi.bool(),
|
||||
hooks: joi.object({
|
||||
beforeDuplicate: joi.func(),
|
||||
}),
|
||||
listSearchableFields: joi.array().items(joi.string()),
|
||||
livePreview: joi.object(livePreviewSchema),
|
||||
pagination: joi.object({
|
||||
|
||||
@@ -195,15 +195,6 @@ export type AfterForgotPasswordHook = (args: {
|
||||
context: RequestContext
|
||||
}) => 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 = {
|
||||
/**
|
||||
* Custom admin components
|
||||
@@ -275,12 +266,6 @@ export type CollectionAdminOptions = {
|
||||
* Hide the API URL within the Edit view
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
||||
366
packages/payload/src/collections/operations/duplicate.ts
Normal file
366
packages/payload/src/collections/operations/duplicate.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import auth from '../../../auth/operations/local/index.js'
|
||||
import create from './create.js'
|
||||
import deleteLocal from './delete.js'
|
||||
import { duplicate } from './duplicate.js'
|
||||
import find from './find.js'
|
||||
import findByID from './findByID.js'
|
||||
import findVersionByID from './findVersionByID.js'
|
||||
@@ -12,6 +13,7 @@ export default {
|
||||
auth,
|
||||
create,
|
||||
deleteLocal,
|
||||
duplicate,
|
||||
find,
|
||||
findByID,
|
||||
findVersionByID,
|
||||
|
||||
@@ -27,7 +27,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
|
||||
|
||||
export type ServerOnlyCollectionAdminProperties = keyof Pick<
|
||||
SanitizedCollectionConfig['admin'],
|
||||
'components' | 'hidden' | 'hooks' | 'preview'
|
||||
'components' | 'hidden' | 'preview'
|
||||
>
|
||||
|
||||
export type ServerOnlyGlobalProperties = keyof Pick<
|
||||
@@ -193,7 +193,6 @@ const sanitizeCollections = (
|
||||
'components',
|
||||
'hidden',
|
||||
'preview',
|
||||
'hooks',
|
||||
// `livePreview` is handled separately
|
||||
]
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export { createOperation } from '../collections/operations/create.js'
|
||||
export { deleteOperation } from '../collections/operations/delete.js'
|
||||
export { deleteByIDOperation } from '../collections/operations/deleteByID.js'
|
||||
export { docAccessOperation } from '../collections/operations/docAccess.js'
|
||||
export { duplicateOperation } from '../collections/operations/duplicate.js'
|
||||
export { findOperation } from '../collections/operations/find.js'
|
||||
export { findByIDOperation } from '../collections/operations/findByID.js'
|
||||
export { findVersionByIDOperation } from '../collections/operations/findVersionByID.js'
|
||||
|
||||
@@ -25,7 +25,6 @@ export type {
|
||||
AfterReadHook as CollectionAfterReadHook,
|
||||
BeforeChangeHook as CollectionBeforeChangeHook,
|
||||
BeforeDeleteHook as CollectionBeforeDeleteHook,
|
||||
BeforeDuplicate,
|
||||
BeforeLoginHook as CollectionBeforeLoginHook,
|
||||
BeforeOperationHook as CollectionBeforeOperationHook,
|
||||
BeforeReadHook as CollectionBeforeReadHook,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
||||
import { baseIDField } from '../baseFields/baseIDField.js'
|
||||
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
||||
import validations from '../validations.js'
|
||||
import { fieldAffectsData, tabHasName } from './types.js'
|
||||
|
||||
@@ -129,6 +130,8 @@ export const sanitizeFields = ({
|
||||
|
||||
if (!field.hooks) field.hooks = {}
|
||||
if (!field.access) field.access = {}
|
||||
|
||||
setDefaultBeforeDuplicate(field)
|
||||
}
|
||||
|
||||
if (!field.admin) {
|
||||
|
||||
@@ -45,6 +45,7 @@ export const baseField = joi
|
||||
afterChange: joi.array().items(joi.func()).default([]),
|
||||
afterRead: 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([]),
|
||||
})
|
||||
.default(),
|
||||
|
||||
@@ -183,6 +183,10 @@ export interface FieldBase {
|
||||
afterChange?: FieldHook[]
|
||||
afterRead?: 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[]
|
||||
}
|
||||
index?: boolean
|
||||
|
||||
@@ -10,6 +10,7 @@ type Args<T> = {
|
||||
context: RequestContext
|
||||
data: Record<string, unknown> | T
|
||||
doc?: Record<string, unknown> | T
|
||||
duplicate?: boolean
|
||||
global: SanitizedGlobalConfig | null
|
||||
id?: number | string
|
||||
operation: 'create' | 'update'
|
||||
@@ -23,6 +24,7 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
|
||||
context,
|
||||
data: incomingData,
|
||||
doc,
|
||||
duplicate = false,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
@@ -36,6 +38,7 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields: collection?.fields || global?.fields,
|
||||
global,
|
||||
operation,
|
||||
|
||||
@@ -15,6 +15,7 @@ type Args<T> = {
|
||||
context: RequestContext
|
||||
data: T
|
||||
doc: T
|
||||
duplicate: boolean
|
||||
field: Field | TabAsField
|
||||
global: SanitizedGlobalConfig | null
|
||||
id?: number | string
|
||||
@@ -38,6 +39,7 @@ export const promise = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
field,
|
||||
global,
|
||||
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
|
||||
@@ -276,6 +304,7 @@ export const promise = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
@@ -301,6 +330,7 @@ export const promise = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
@@ -336,6 +366,7 @@ export const promise = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields: block.fields,
|
||||
global,
|
||||
operation,
|
||||
@@ -361,6 +392,7 @@ export const promise = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
@@ -393,6 +425,7 @@ export const promise = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
@@ -412,6 +445,7 @@ export const promise = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
operation,
|
||||
|
||||
@@ -10,6 +10,7 @@ type Args<T> = {
|
||||
context: RequestContext
|
||||
data: T
|
||||
doc: T
|
||||
duplicate: boolean
|
||||
fields: (Field | TabAsField)[]
|
||||
global: SanitizedGlobalConfig | null
|
||||
id?: number | string
|
||||
@@ -26,6 +27,7 @@ export const traverseFields = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
fields,
|
||||
global,
|
||||
operation,
|
||||
@@ -43,6 +45,7 @@ export const traverseFields = async <T>({
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
duplicate,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
|
||||
33
packages/payload/src/fields/setDefaultBeforeDuplicate.ts
Normal file
33
packages/payload/src/fields/setDefaultBeforeDuplicate.ts
Normal 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
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 FindVersionsOptions } from './collections/operations/local/findVersions.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 {
|
||||
ByIDOptions as UpdateByIDOptions,
|
||||
ManyOptions as UpdateManyOptions,
|
||||
@@ -411,6 +412,13 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
const { update } = localOperations
|
||||
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()
|
||||
|
||||
@@ -38,12 +38,14 @@ export default {
|
||||
textToDisplay: 'النصّ الذي تريد إظهاره',
|
||||
},
|
||||
general: {
|
||||
copy: 'نسخ',
|
||||
createdAt: 'تمّ الإنشاء في',
|
||||
deletedCountSuccessfully: 'تمّ حذف {{count}} {{label}} بنجاح.',
|
||||
deletedSuccessfully: 'تمّ الحذف بنجاح.',
|
||||
email: 'البريد الإلكتروني',
|
||||
notFound: 'غير موجود',
|
||||
successfullyCreated: '{{label}} تم إنشاؤها بنجاح.',
|
||||
successfullyDuplicated: '{{label}} تم استنساخها بنجاح.',
|
||||
thisLanguage: 'العربية',
|
||||
updatedAt: 'تم التحديث في',
|
||||
updatedCountSuccessfully: 'تم تحديث {{count}} {{label}} بنجاح.',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'Göstəriləcək mətn',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopyala',
|
||||
createdAt: 'Yaradıldığı tarix',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} uğurla silindi.',
|
||||
deletedSuccessfully: 'Uğurla silindi.',
|
||||
email: 'Elektron poçt',
|
||||
notFound: 'Tapılmadı',
|
||||
successfullyCreated: '{{label}} uğurla yaradıldı.',
|
||||
successfullyDuplicated: '{{label}} uğurla dublikatlandı.',
|
||||
thisLanguage: 'Azərbaycan dili',
|
||||
updatedAt: 'Yeniləndiyi tarix',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} uğurla yeniləndi.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Текст към дисплей',
|
||||
},
|
||||
general: {
|
||||
copy: 'Копирай',
|
||||
createdAt: 'Създаден на',
|
||||
deletedCountSuccessfully: 'Изтрити {{count}} {{label}} успешно.',
|
||||
deletedSuccessfully: 'Изтрито успешно.',
|
||||
email: 'Имейл',
|
||||
notFound: 'Няма открит',
|
||||
successfullyCreated: '{{label}} успешно създаден.',
|
||||
successfullyDuplicated: '{{label}} успешно дупликиран.',
|
||||
thisLanguage: 'Български',
|
||||
updatedAt: 'Обновен на',
|
||||
updatedCountSuccessfully: 'Обновени {{count}} {{label}} успешно.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Text k zobrazení',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopírovat',
|
||||
createdAt: 'Vytvořeno v',
|
||||
deletedCountSuccessfully: 'Úspěšně smazáno {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Úspěšně odstraněno.',
|
||||
email: 'E-mail',
|
||||
notFound: 'Nenalezeno',
|
||||
successfullyCreated: '{{label}} úspěšně vytvořeno.',
|
||||
successfullyDuplicated: '{{label}} úspěšně duplikováno.',
|
||||
thisLanguage: 'Čeština',
|
||||
updatedAt: 'Aktualizováno v',
|
||||
updatedCountSuccessfully: 'Úspěšně aktualizováno {{count}} {{label}}.',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'Angezeigter Text',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopieren',
|
||||
createdAt: 'Erstellt am',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} erfolgreich gelöscht.',
|
||||
deletedSuccessfully: 'Erfolgreich gelöscht.',
|
||||
email: 'E-Mail',
|
||||
notFound: 'Nicht gefunden',
|
||||
successfullyCreated: '{{label}} erfolgreich erstellt.',
|
||||
successfullyDuplicated: '{{label}} wurde erfolgreich dupliziert.',
|
||||
thisLanguage: 'Deutsch',
|
||||
updatedAt: 'Aktualisiert am',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} erfolgreich aktualisiert.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Text to display',
|
||||
},
|
||||
general: {
|
||||
copy: 'Copy',
|
||||
createdAt: 'Created At',
|
||||
deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.',
|
||||
deletedSuccessfully: 'Deleted successfully.',
|
||||
email: 'Email',
|
||||
notFound: 'Not Found',
|
||||
successfullyCreated: '{{label}} successfully created.',
|
||||
successfullyDuplicated: '{{label}} successfully duplicated.',
|
||||
thisLanguage: 'English',
|
||||
updatedAt: 'Updated At',
|
||||
updatedCountSuccessfully: 'Updated {{count}} {{label}} successfully.',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'Texto a mostrar',
|
||||
},
|
||||
general: {
|
||||
copy: 'Copiar',
|
||||
createdAt: 'Fecha de creación',
|
||||
deletedCountSuccessfully: 'Se eliminó {{count}} {{label}} con éxito.',
|
||||
deletedSuccessfully: 'Borrado exitosamente.',
|
||||
email: 'Correo electrónico',
|
||||
notFound: 'No encontrado',
|
||||
successfullyCreated: '{{label}} creado correctamente.',
|
||||
successfullyDuplicated: '{{label}} duplicado correctamente.',
|
||||
thisLanguage: 'Español',
|
||||
updatedAt: 'Fecha de modificado',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} actualizado con éxito.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'متن برای نمایش',
|
||||
},
|
||||
general: {
|
||||
copy: 'رونوشت',
|
||||
createdAt: 'ساخته شده در',
|
||||
deletedCountSuccessfully: 'تعداد {{count}} {{label}} با موفقیت پاک گردید.',
|
||||
deletedSuccessfully: 'با موفقیت حذف شد.',
|
||||
email: 'رایانامه',
|
||||
notFound: 'یافت نشد',
|
||||
successfullyCreated: '{{label}} با موفقیت ساخته شد.',
|
||||
successfullyDuplicated: '{{label}} با موفقیت رونوشت شد.',
|
||||
thisLanguage: 'فارسی',
|
||||
updatedAt: 'بروز شده در',
|
||||
updatedCountSuccessfully: 'تعداد {{count}} با عنوان {{label}} با موفقیت بروزرسانی شدند.',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'Texte à afficher',
|
||||
},
|
||||
general: {
|
||||
copy: 'Copie',
|
||||
createdAt: 'Créé(e) à',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} supprimé avec succès.',
|
||||
deletedSuccessfully: 'Supprimé(e) avec succès.',
|
||||
email: 'E-mail',
|
||||
notFound: 'Pas trouvé',
|
||||
successfullyCreated: '{{label}} créé(e) avec succès.',
|
||||
successfullyDuplicated: '{{label}} dupliqué(e) avec succès.',
|
||||
thisLanguage: 'Français',
|
||||
updatedAt: 'Modifié le',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} mis à jour avec succès.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Tekst za prikaz',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopiraj',
|
||||
createdAt: 'Kreirano u',
|
||||
deletedCountSuccessfully: 'Uspješno izbrisano {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Uspješno obrisano.',
|
||||
email: 'Email',
|
||||
notFound: 'Nije pronađeno',
|
||||
successfullyCreated: '{{label}} uspješno kreirano.',
|
||||
successfullyDuplicated: '{{label}} uspješno duplicirano.',
|
||||
thisLanguage: 'Hrvatski',
|
||||
updatedAt: 'Ažurirano u',
|
||||
updatedCountSuccessfully: 'Uspješno ažurirano {{count}} {{label}}.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Megjelenítendő szöveg',
|
||||
},
|
||||
general: {
|
||||
copy: 'Másolás',
|
||||
createdAt: 'Létrehozva:',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} sikeresen törölve.',
|
||||
deletedSuccessfully: 'Sikeresen törölve.',
|
||||
email: 'E-mail',
|
||||
notFound: 'Nem található',
|
||||
successfullyCreated: '{{label}} sikeresen létrehozva.',
|
||||
successfullyDuplicated: '{{label}} sikeresen duplikálódott.',
|
||||
thisLanguage: 'Magyar',
|
||||
updatedAt: 'Frissítve:',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} sikeresen frissítve.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Testo da visualizzare',
|
||||
},
|
||||
general: {
|
||||
copy: 'Copia',
|
||||
createdAt: 'Creato il',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} eliminato con successo.',
|
||||
deletedSuccessfully: 'Eliminato con successo.',
|
||||
email: 'Email',
|
||||
notFound: 'Non Trovato',
|
||||
successfullyCreated: '{{label}} creato con successo.',
|
||||
successfullyDuplicated: '{{label}} duplicato con successo.',
|
||||
thisLanguage: 'Italiano',
|
||||
updatedAt: 'Aggiornato il',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} aggiornato con successo.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: '表示するテキスト',
|
||||
},
|
||||
general: {
|
||||
copy: 'コピー',
|
||||
createdAt: '作成日',
|
||||
deletedCountSuccessfully: '{{count}}つの{{label}}を正常に削除しました。',
|
||||
deletedSuccessfully: '正常に削除されました。',
|
||||
email: 'メールアドレス',
|
||||
notFound: 'Not Found',
|
||||
successfullyCreated: '{{label}} が作成されました。',
|
||||
successfullyDuplicated: '{{label}} が複製されました。',
|
||||
thisLanguage: 'Japanese',
|
||||
updatedAt: '更新日',
|
||||
updatedCountSuccessfully: '{{count}}つの{{label}}を正常に更新しました。',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: '표시할 텍스트',
|
||||
},
|
||||
general: {
|
||||
copy: '복사',
|
||||
createdAt: '생성 일시',
|
||||
deletedCountSuccessfully: '{{count}}개의 {{label}}를 삭제했습니다.',
|
||||
deletedSuccessfully: '삭제되었습니다.',
|
||||
email: '이메일',
|
||||
notFound: '찾을 수 없음',
|
||||
successfullyCreated: '{{label}}이(가) 생성되었습니다.',
|
||||
successfullyDuplicated: '{{label}}이(가) 복제되었습니다.',
|
||||
thisLanguage: '한국어',
|
||||
updatedAt: '업데이트 일시',
|
||||
updatedCountSuccessfully: '{{count}}개의 {{label}}을(를) 업데이트했습니다.',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'ပြသရန် စာသား',
|
||||
},
|
||||
general: {
|
||||
copy: 'ကူးယူမည်။',
|
||||
createdAt: 'ဖန်တီးခဲ့သည့်အချိန်',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။',
|
||||
deletedSuccessfully: 'အောင်မြင်စွာ ဖျက်လိုက်ပါပြီ။',
|
||||
email: 'အီးမေးလ်',
|
||||
notFound: 'ဘာမှ မရှိတော့ဘူး။',
|
||||
successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။',
|
||||
successfullyDuplicated: '{{label}} အောင်မြင်စွာ ပုံတူပွားခဲ့သည်။',
|
||||
thisLanguage: 'မြန်မာစာ',
|
||||
updatedAt: 'ပြင်ဆင်ခဲ့သည့်အချိန်',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} ကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ခဲ့သည်။',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Tekst som skal vises',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopiér',
|
||||
createdAt: 'Opprettet',
|
||||
deletedCountSuccessfully: 'Slettet {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Slettet.',
|
||||
email: 'E-post',
|
||||
notFound: 'Ikke funnet',
|
||||
successfullyCreated: '{{label}} ble opprettet.',
|
||||
successfullyDuplicated: '{{label}} ble duplisert.',
|
||||
thisLanguage: 'Norsk',
|
||||
updatedAt: 'Oppdatert',
|
||||
updatedCountSuccessfully: 'Oppdaterte {{count}} {{label}} vellykket.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Tekst om weer te geven',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopiëren',
|
||||
createdAt: 'Aangemaakt op',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} succesvol verwijderd.',
|
||||
deletedSuccessfully: 'Succesvol verwijderd.',
|
||||
email: 'E-mail',
|
||||
notFound: 'Niet gevonden',
|
||||
successfullyCreated: '{{label}} succesvol aangemaakt.',
|
||||
successfullyDuplicated: '{{label}} succesvol gedupliceerd.',
|
||||
thisLanguage: 'Nederlands',
|
||||
updatedAt: 'Aangepast op',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} succesvol bijgewerkt.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Tekst do wyświetlenia',
|
||||
},
|
||||
general: {
|
||||
copy: 'Skopiuj',
|
||||
createdAt: 'Data utworzenia',
|
||||
deletedCountSuccessfully: 'Pomyślnie usunięto {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Pomyślnie usunięto.',
|
||||
email: 'Email',
|
||||
notFound: 'Nie znaleziono',
|
||||
successfullyCreated: 'Pomyślnie utworzono {{label}}.',
|
||||
successfullyDuplicated: 'Pomyślnie zduplikowano {{label}}',
|
||||
thisLanguage: 'Polski',
|
||||
updatedAt: 'Data edycji',
|
||||
updatedCountSuccessfully: 'Pomyślnie zaktualizowano {{count}} {{label}}.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Texto a ser exibido',
|
||||
},
|
||||
general: {
|
||||
copy: 'Copiar',
|
||||
createdAt: 'Criado Em',
|
||||
deletedCountSuccessfully: 'Excluído {{count}} {{label}} com sucesso.',
|
||||
deletedSuccessfully: 'Apagado com sucesso.',
|
||||
email: 'Email',
|
||||
notFound: 'Não Encontrado',
|
||||
successfullyCreated: '{{label}} criado com sucesso.',
|
||||
successfullyDuplicated: '{{label}} duplicado com sucesso.',
|
||||
thisLanguage: 'Português',
|
||||
updatedAt: 'Atualizado Em',
|
||||
updatedCountSuccessfully: 'Atualizado {{count}} {{label}} com sucesso.',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'Text de afișat',
|
||||
},
|
||||
general: {
|
||||
copy: 'Copiați',
|
||||
createdAt: 'Creat la',
|
||||
deletedCountSuccessfully: 'Șterse cu succes {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Șters cu succes.',
|
||||
email: 'Email',
|
||||
notFound: 'Nu a fost găsit',
|
||||
successfullyCreated: '{{label}} creat(ă) cu succes.',
|
||||
successfullyDuplicated: '{{label}} duplicat(ă) cu succes.',
|
||||
thisLanguage: 'Română',
|
||||
updatedAt: 'Actualizat la',
|
||||
updatedCountSuccessfully: 'Actualizate {{count}} {{label}} cu succes.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Tekst za prikaz',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopiraj',
|
||||
createdAt: 'Kreirano u',
|
||||
deletedCountSuccessfully: 'Uspešno izbrisano {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Uspešno izbrisano.',
|
||||
email: 'E-pošta',
|
||||
notFound: 'Nije pronađeno',
|
||||
successfullyCreated: '{{label}} uspešno kreirano.',
|
||||
successfullyDuplicated: '{{label}} uspešno duplicirano.',
|
||||
thisLanguage: 'Srpski (latinica)',
|
||||
updatedAt: 'Ažurirano u',
|
||||
updatedCountSuccessfully: 'Uspešno ažurirano {{count}} {{label}}.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Текст за приказ',
|
||||
},
|
||||
general: {
|
||||
copy: 'Копирај',
|
||||
createdAt: 'Креирано у',
|
||||
deletedCountSuccessfully: 'Успешно избрисано {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Успешно избрисано.',
|
||||
email: 'Е-пошта',
|
||||
notFound: 'Није пронађено',
|
||||
successfullyCreated: '{{label}} успешно креирано.',
|
||||
successfullyDuplicated: '{{label}} успешно дуплицирано.',
|
||||
thisLanguage: 'Српски (ћирилица)',
|
||||
updatedAt: 'Ажурирано у',
|
||||
updatedCountSuccessfully: 'Успешно ажурирано {{count}} {{label}}.',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'Текст для отображения',
|
||||
},
|
||||
general: {
|
||||
copy: 'Скопировать',
|
||||
createdAt: 'Дата создания',
|
||||
deletedCountSuccessfully: 'Удалено {{count}} {{label}} успешно.',
|
||||
deletedSuccessfully: 'Удален успешно.',
|
||||
email: 'Email',
|
||||
notFound: 'Не найдено',
|
||||
successfullyCreated: '{{label}} успешно создан.',
|
||||
successfullyDuplicated: '{{label}} успешно продублирован.',
|
||||
thisLanguage: 'Русский',
|
||||
updatedAt: 'Дата правки',
|
||||
updatedCountSuccessfully: 'Обновлено {{count}} {{label}} успешно.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Text att visa',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopiera',
|
||||
createdAt: 'Skapad Vid',
|
||||
deletedCountSuccessfully: 'Raderade {{count}} {{label}} framgångsrikt.',
|
||||
deletedSuccessfully: 'Togs bort framgångsrikt.',
|
||||
email: 'E-post',
|
||||
notFound: 'Hittades inte',
|
||||
successfullyCreated: '{{label}} skapades framgångsrikt.',
|
||||
successfullyDuplicated: '{{label}} duplicerades framgångsrikt.',
|
||||
thisLanguage: 'Svenska',
|
||||
updatedAt: 'Uppdaterades Vid',
|
||||
updatedCountSuccessfully: 'Uppdaterade {{count}} {{label}} framgångsrikt.',
|
||||
|
||||
@@ -38,12 +38,14 @@ export default {
|
||||
textToDisplay: 'ข้อความสำหรับแสดงผล',
|
||||
},
|
||||
general: {
|
||||
copy: 'คัดลอก',
|
||||
createdAt: 'สร้างเมื่อ',
|
||||
deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.',
|
||||
deletedSuccessfully: 'ลบสำเร็จ',
|
||||
email: 'อีเมล',
|
||||
notFound: 'ไม่พบ',
|
||||
successfullyCreated: 'สร้าง {{label}} สำเร็จ',
|
||||
successfullyDuplicated: 'สำเนา {{label}} สำเร็จ',
|
||||
thisLanguage: 'ไทย',
|
||||
updatedAt: 'แก้ไขเมื่อ',
|
||||
updatedCountSuccessfully: 'อัปเดต {{count}} {{label}} เรียบร้อยแล้ว',
|
||||
|
||||
@@ -40,12 +40,14 @@ export default {
|
||||
textToDisplay: 'Görüntülenecek metin',
|
||||
},
|
||||
general: {
|
||||
copy: 'Kopyala',
|
||||
createdAt: 'Oluşturma tarihi',
|
||||
deletedCountSuccessfully: '{{count}} {{label}} başarıyla silindi.',
|
||||
deletedSuccessfully: 'Başarıyla silindi.',
|
||||
email: 'E-posta',
|
||||
notFound: 'Bulunamadı',
|
||||
successfullyCreated: '{{label}} başarıyla oluşturuldu.',
|
||||
successfullyDuplicated: '{{label}} başarıyla kopyalandı.',
|
||||
thisLanguage: 'Türkçe',
|
||||
updatedAt: 'Güncellenme tarihi',
|
||||
updatedCountSuccessfully: '{{count}} {{label}} başarıyla güncellendi.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Текст для відображення',
|
||||
},
|
||||
general: {
|
||||
copy: 'Скопіювати',
|
||||
createdAt: 'Дата створення',
|
||||
deletedCountSuccessfully: 'Успішно видалено {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Успішно видалено.',
|
||||
email: 'Email',
|
||||
notFound: 'Не знайдено',
|
||||
successfullyCreated: '{{label}} успішно створено.',
|
||||
successfullyDuplicated: '{{label}} успішно продубльовано.',
|
||||
thisLanguage: 'Українська',
|
||||
updatedAt: 'Змінено',
|
||||
updatedCountSuccessfully: 'Успішно оновлено {{count}} {{label}}.',
|
||||
|
||||
@@ -39,12 +39,14 @@ export default {
|
||||
textToDisplay: 'Văn bản để hiển thị',
|
||||
},
|
||||
general: {
|
||||
copy: 'Sao chép',
|
||||
createdAt: 'Ngày tạo',
|
||||
deletedCountSuccessfully: 'Đã xóa thành công {{count}} {{label}}.',
|
||||
deletedSuccessfully: 'Đã xoá thành công.',
|
||||
email: 'Email',
|
||||
notFound: 'Không tìm thấy',
|
||||
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)',
|
||||
updatedAt: 'Ngày cập nhật',
|
||||
updatedCountSuccessfully: 'Đã cập nhật thành công {{count}} {{label}}.',
|
||||
|
||||
@@ -37,12 +37,14 @@ export default {
|
||||
textToDisplay: '要顯示的文字',
|
||||
},
|
||||
general: {
|
||||
copy: '複製',
|
||||
createdAt: '建立於',
|
||||
deletedCountSuccessfully: '已成功刪除 {{count}} 個 {{label}}。',
|
||||
deletedSuccessfully: '已成功刪除。',
|
||||
email: '電子郵件',
|
||||
notFound: '未找到',
|
||||
successfullyCreated: '成功建立{{label}}',
|
||||
successfullyDuplicated: '成功複製{{label}}',
|
||||
thisLanguage: '中文 (繁體)',
|
||||
updatedAt: '更新於',
|
||||
updatedCountSuccessfully: '已成功更新 {{count}} 個 {{label}}。',
|
||||
|
||||
@@ -37,12 +37,14 @@ export default {
|
||||
textToDisplay: '要显示的文本',
|
||||
},
|
||||
general: {
|
||||
copy: '复制',
|
||||
createdAt: '创建于',
|
||||
deletedCountSuccessfully: '已成功删除 {{count}} {{label}}。',
|
||||
deletedSuccessfully: '已成功删除。',
|
||||
email: '电子邮件',
|
||||
notFound: '未找到',
|
||||
successfullyCreated: '成功创建{{label}}',
|
||||
successfullyDuplicated: '成功复制{{label}}',
|
||||
thisLanguage: '中文 (简体)',
|
||||
updatedAt: '更新于',
|
||||
updatedCountSuccessfully: '已成功更新 {{count}} {{label}}。',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'تمت الإرسال بنجاح.',
|
||||
submit: 'إرسال',
|
||||
successfullyCreated: '{{label}} تم إنشاؤها بنجاح.',
|
||||
successfullyDuplicated: '{{label}} تم استنساخها بنجاح.',
|
||||
thisLanguage: 'العربية',
|
||||
titleDeleted: 'تم حذف {{label}} "{{title}}" بنجاح.',
|
||||
unauthorized: 'غير مصرح به',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Təqdimat uğurlu oldu.',
|
||||
submit: 'Təqdim et',
|
||||
successfullyCreated: '{{label}} uğurla yaradıldı.',
|
||||
successfullyDuplicated: '{{label}} uğurla dublikatlandı.',
|
||||
thisLanguage: 'Azərbaycan dili',
|
||||
titleDeleted: '{{label}} "{{title}}" uğurla silindi.',
|
||||
unauthorized: 'İcazəsiz',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'Успешно подаване.',
|
||||
submit: 'Подай',
|
||||
successfullyCreated: '{{label}} успешно създаден.',
|
||||
successfullyDuplicated: '{{label}} успешно дупликиран.',
|
||||
thisLanguage: 'Български',
|
||||
titleDeleted: '{{label}} "{{title}}" успешно изтрит.',
|
||||
unauthorized: 'Неавторизиран',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'Odeslání úspěšné.',
|
||||
submit: 'Odeslat',
|
||||
successfullyCreated: '{{label}} úspěšně vytvořeno.',
|
||||
successfullyDuplicated: '{{label}} úspěšně duplikováno.',
|
||||
thisLanguage: 'Čeština',
|
||||
titleDeleted: '{{label}} "{{title}}" úspěšně smazáno.',
|
||||
unauthorized: 'Neoprávněný',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Einrichung erfolgreich.',
|
||||
submit: 'Senden',
|
||||
successfullyCreated: '{{label}} erfolgreich erstellt.',
|
||||
successfullyDuplicated: '{{label}} wurde erfolgreich dupliziert.',
|
||||
thisLanguage: 'Deutsch',
|
||||
titleDeleted: '{{label}} {{title}} wurde erfolgreich gelöscht.',
|
||||
unauthorized: 'Nicht autorisiert',
|
||||
|
||||
@@ -47,8 +47,6 @@ export default {
|
||||
deletingTitle:
|
||||
'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}}.',
|
||||
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}}"',
|
||||
notAllowedToAccessPage: 'You are not allowed to access this page.',
|
||||
previewing: 'There was a problem previewing this document.',
|
||||
@@ -191,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Submission Successful.',
|
||||
submit: 'Submit',
|
||||
successfullyCreated: '{{label}} successfully created.',
|
||||
successfullyDuplicated: '{{label}} successfully duplicated.',
|
||||
thisLanguage: 'English',
|
||||
titleDeleted: '{{label}} "{{title}}" successfully deleted.',
|
||||
unauthorized: 'Unauthorized',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Envío realizado correctamente.',
|
||||
submit: 'Enviar',
|
||||
successfullyCreated: '{{label}} creado correctamente.',
|
||||
successfullyDuplicated: '{{label}} duplicado correctamente.',
|
||||
thisLanguage: 'Español',
|
||||
titleDeleted: '{{label}} {{title}} eliminado correctamente.',
|
||||
unauthorized: 'No autorizado',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'با موفقیت ثبت شد.',
|
||||
submit: 'فرستادن',
|
||||
successfullyCreated: '{{label}} با موفقیت ساخته شد.',
|
||||
successfullyDuplicated: '{{label}} با موفقیت رونوشت شد.',
|
||||
thisLanguage: 'فارسی',
|
||||
titleDeleted: '{{label}} "{{title}}" با موفقیت پاک شد.',
|
||||
unauthorized: 'غیرمجاز',
|
||||
|
||||
@@ -191,7 +191,6 @@ export default {
|
||||
submissionSuccessful: 'Soumission réussie.',
|
||||
submit: 'Soumettre',
|
||||
successfullyCreated: '{{label}} créé(e) avec succès.',
|
||||
successfullyDuplicated: '{{label}} dupliqué(e) avec succès.',
|
||||
thisLanguage: 'Français',
|
||||
titleDeleted: '{{label}} "{{title}}" supprimé(e) avec succès.',
|
||||
unauthorized: 'Non autorisé',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'Uspješno slanje',
|
||||
submit: 'Podnesi',
|
||||
successfullyCreated: '{{label}} uspješno kreirano.',
|
||||
successfullyDuplicated: '{{label}} uspješno duplicirano.',
|
||||
thisLanguage: 'Hrvatski',
|
||||
titleDeleted: '{{label}} "{{title}}" uspješno obrisano.',
|
||||
unauthorized: 'Neovlašteno',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Beküldés sikeres.',
|
||||
submit: 'Beküldés',
|
||||
successfullyCreated: '{{label}} sikeresen létrehozva.',
|
||||
successfullyDuplicated: '{{label}} sikeresen duplikálódott.',
|
||||
thisLanguage: 'Magyar',
|
||||
titleDeleted: '{{label}} "{{title}}" sikeresen törölve.',
|
||||
unauthorized: 'Jogosulatlan',
|
||||
|
||||
@@ -190,7 +190,6 @@ export default {
|
||||
submissionSuccessful: 'Invio riuscito.',
|
||||
submit: 'Invia',
|
||||
successfullyCreated: '{{label}} creato con successo.',
|
||||
successfullyDuplicated: '{{label}} duplicato con successo.',
|
||||
thisLanguage: 'Italiano',
|
||||
titleDeleted: '{{label}} {{title}} eliminato con successo.',
|
||||
unauthorized: 'Non autorizzato',
|
||||
|
||||
@@ -47,8 +47,6 @@ export default {
|
||||
deletingTitle:
|
||||
'{{title}} を削除する際にエラーが発生しました。接続を確認してからもう一度お試しください。',
|
||||
loadingDocument: 'IDが {{id}} のデータを読み込む際に問題が発生しました。',
|
||||
localesNotSaved_one: '次のロケールは保存できませんでした:',
|
||||
localesNotSaved_other: '次のロケールは保存できませんでした:',
|
||||
noMatchedField: '"{{label}}" に該当するフィールドがありません。',
|
||||
notAllowedToAccessPage: 'この画面へのアクセスは許可されていません。',
|
||||
previewing: 'このデータをプレビューする際に問題が発生しました。',
|
||||
@@ -190,7 +188,6 @@ export default {
|
||||
submissionSuccessful: '送信が成功しました。',
|
||||
submit: '送信',
|
||||
successfullyCreated: '{{label}} が作成されました。',
|
||||
successfullyDuplicated: '{{label}} が複製されました。',
|
||||
thisLanguage: 'Japanese',
|
||||
titleDeleted: '{{label}} "{{title}}" が削除されました。',
|
||||
unauthorized: '未認証',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: '제출이 완료되었습니다.',
|
||||
submit: '제출',
|
||||
successfullyCreated: '{{label}}이(가) 생성되었습니다.',
|
||||
successfullyDuplicated: '{{label}}이(가) 복제되었습니다.',
|
||||
thisLanguage: '한국어',
|
||||
titleDeleted: '{{label}} "{{title}}"을(를) 삭제했습니다.',
|
||||
unauthorized: '권한 없음',
|
||||
|
||||
@@ -190,7 +190,6 @@ export default {
|
||||
submissionSuccessful: 'သိမ်းဆည်းမှု အောင်မြင်ပါသည်။',
|
||||
submit: 'သိမ်းဆည်းမည်။',
|
||||
successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။',
|
||||
successfullyDuplicated: '{{label}} အောင်မြင်စွာ ပုံတူပွားခဲ့သည်။',
|
||||
thisLanguage: 'မြန်မာစာ',
|
||||
titleDeleted: '{{label}} {{title}} အောင်မြင်စွာ ဖျက်သိမ်းခဲ့သည်။',
|
||||
unauthorized: 'အခွင့်မရှိပါ။',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Innsending vellykket.',
|
||||
submit: 'Send inn',
|
||||
successfullyCreated: '{{label}} ble opprettet.',
|
||||
successfullyDuplicated: '{{label}} ble duplisert.',
|
||||
thisLanguage: 'Norsk',
|
||||
titleDeleted: '{{label}} "{{title}}" ble slettet.',
|
||||
unauthorized: 'Ikke autorisert',
|
||||
|
||||
@@ -190,7 +190,6 @@ export default {
|
||||
submissionSuccessful: 'Indiening succesvol.',
|
||||
submit: 'Indienen',
|
||||
successfullyCreated: '{{label}} succesvol aangemaakt.',
|
||||
successfullyDuplicated: '{{label}} succesvol gedupliceerd.',
|
||||
thisLanguage: 'Nederlands',
|
||||
titleDeleted: '{{label}} "{{title}}" succesvol verwijderd.',
|
||||
unauthorized: 'Onbevoegd',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Zgłoszenie zakończone powodzeniem.',
|
||||
submit: 'Zatwierdź',
|
||||
successfullyCreated: 'Pomyślnie utworzono {{label}}.',
|
||||
successfullyDuplicated: 'Pomyślnie zduplikowano {{label}}',
|
||||
thisLanguage: 'Polski',
|
||||
titleDeleted: 'Pomyślnie usunięto {{label}} {{title}}',
|
||||
unauthorized: 'Brak autoryzacji',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Envio bem-sucedido.',
|
||||
submit: 'Enviar',
|
||||
successfullyCreated: '{{label}} criado com sucesso.',
|
||||
successfullyDuplicated: '{{label}} duplicado com sucesso.',
|
||||
thisLanguage: 'Português',
|
||||
titleDeleted: '{{label}} {{title}} excluído com sucesso.',
|
||||
unauthorized: 'Não autorizado',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Trimitere cu succes.',
|
||||
submit: 'Trimite',
|
||||
successfullyCreated: '{{label}} creat(ă) cu succes.',
|
||||
successfullyDuplicated: '{{label}} duplicat(ă) cu succes.',
|
||||
thisLanguage: 'Română',
|
||||
titleDeleted: '{{label}} "{{title}}" șters cu succes.',
|
||||
unauthorized: 'neautorizat(ă)',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'Uspešno slanje',
|
||||
submit: 'Potvrdi',
|
||||
successfullyCreated: '{{label}} uspešno kreirano.',
|
||||
successfullyDuplicated: '{{label}} uspešno duplicirano.',
|
||||
thisLanguage: 'Srpski (latinica)',
|
||||
titleDeleted: '{{label}} "{{title}}" uspešno obrisano.',
|
||||
unauthorized: 'Niste autorizovani',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'Успешно слање',
|
||||
submit: 'Потврди',
|
||||
successfullyCreated: '{{label}} успешно креирано.',
|
||||
successfullyDuplicated: '{{label}} успешно дуплицирано.',
|
||||
thisLanguage: 'Српски (ћирилица)',
|
||||
titleDeleted: '{{label}} "{{title}}" успешно обрисано.',
|
||||
unauthorized: 'Нисте ауторизовани',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Успешно отправлено.',
|
||||
submit: 'Отправить',
|
||||
successfullyCreated: '{{label}} успешно создан.',
|
||||
successfullyDuplicated: '{{label}} успешно продублирован.',
|
||||
thisLanguage: 'Русский',
|
||||
titleDeleted: '{{label}} {{title}} успешно удалено.',
|
||||
unauthorized: 'Нет доступа',
|
||||
|
||||
@@ -189,7 +189,6 @@ export default {
|
||||
submissionSuccessful: 'Inlämningen Lyckades.',
|
||||
submit: 'Lämna in',
|
||||
successfullyCreated: '{{label}} skapades framgångsrikt.',
|
||||
successfullyDuplicated: '{{label}} duplicerades framgångsrikt.',
|
||||
thisLanguage: 'Svenska',
|
||||
titleDeleted: '{{label}} "{{title}}" togs bort framgångsrikt.',
|
||||
unauthorized: 'Obehörig',
|
||||
|
||||
@@ -187,7 +187,6 @@ export default {
|
||||
submissionSuccessful: 'ส่งสำเร็จ',
|
||||
submit: 'ส่ง',
|
||||
successfullyCreated: 'สร้าง {{label}} สำเร็จ',
|
||||
successfullyDuplicated: 'สำเนา {{label}} สำเร็จ',
|
||||
thisLanguage: 'ไทย',
|
||||
titleDeleted: 'ลบ {{label}} "{{title}}" สำเร็จ',
|
||||
unauthorized: 'ไม่ได้รับอนุญาต',
|
||||
|
||||
@@ -190,7 +190,6 @@ export default {
|
||||
submissionSuccessful: 'Gönderme başarılı',
|
||||
submit: 'Gönder',
|
||||
successfullyCreated: '{{label}} başarıyla oluşturuldu.',
|
||||
successfullyDuplicated: '{{label}} başarıyla kopyalandı.',
|
||||
thisLanguage: 'Türkçe',
|
||||
titleDeleted: '{{label}} {{title}} başarıyla silindi.',
|
||||
unauthorized: 'Yetkisiz',
|
||||
|
||||
@@ -188,7 +188,6 @@ export default {
|
||||
submissionSuccessful: 'Успішно відправлено.',
|
||||
submit: 'Відправити',
|
||||
successfullyCreated: '{{label}} успішно створено.',
|
||||
successfullyDuplicated: '{{label}} успішно продубльовано.',
|
||||
thisLanguage: 'Українська',
|
||||
titleDeleted: '{{label}} "{{title}}" успішно видалено.',
|
||||
unauthorized: 'Немає доступу',
|
||||
|
||||
@@ -187,7 +187,6 @@ export default {
|
||||
submissionSuccessful: 'Gửi thành công.',
|
||||
submit: 'Gửi',
|
||||
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)',
|
||||
titleDeleted: '{{label}} {{title}} đã được xóa thành công.',
|
||||
unauthorized: 'Không có quyền truy cập.',
|
||||
|
||||
@@ -46,8 +46,6 @@ export default {
|
||||
correctInvalidFields: '請更正無效區塊。',
|
||||
deletingTitle: '刪除{{title}}時出現了錯誤。請檢查您的網路連線並重試。',
|
||||
loadingDocument: '加載ID為{{id}}的文件時出現了問題。',
|
||||
localesNotSaved_one: '這個語言環境無法被儲存:',
|
||||
localesNotSaved_other: '以下的語言環境無法被儲存:',
|
||||
noMatchedField: '找不到與"{{label}}"匹配的字串',
|
||||
notAllowedToAccessPage: '您沒有權限訪問此頁面。',
|
||||
previewing: '預覽文件時出現了問題。',
|
||||
@@ -188,7 +186,6 @@ export default {
|
||||
submissionSuccessful: '成功送出。',
|
||||
submit: '送出',
|
||||
successfullyCreated: '成功建立{{label}}',
|
||||
successfullyDuplicated: '成功複製{{label}}',
|
||||
thisLanguage: '中文 (繁體)',
|
||||
titleDeleted: '{{label}} "{{title}}"已被成功刪除。',
|
||||
unauthorized: '未經授權',
|
||||
|
||||
@@ -186,7 +186,6 @@ export default {
|
||||
submissionSuccessful: '提交成功。',
|
||||
submit: '提交',
|
||||
successfullyCreated: '成功创建{{label}}',
|
||||
successfullyDuplicated: '成功复制{{label}}',
|
||||
thisLanguage: '中文 (简体)',
|
||||
titleDeleted: '{{label}} "{{title}}"已被成功删除。',
|
||||
unauthorized: '未经授权',
|
||||
|
||||
@@ -77,8 +77,6 @@ export default {
|
||||
invalidFileType: 'Invalid file type',
|
||||
invalidFileTypeValue: 'Invalid file type: {{value}}',
|
||||
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.',
|
||||
missingIDOfDocument: 'Missing ID of document to update.',
|
||||
missingIDOfVersion: 'Missing ID of version.',
|
||||
|
||||
@@ -78,8 +78,6 @@ export default {
|
||||
invalidFileType: '無効なファイル形式',
|
||||
invalidFileTypeValue: '無効なファイル形式: {{value}}',
|
||||
loadingDocument: 'IDが {{id}} のデータを読み込む際に問題が発生しました。',
|
||||
localesNotSaved_one: '次のロケールは保存できませんでした:',
|
||||
localesNotSaved_other: '次のロケールは保存できませんでした:',
|
||||
missingEmail: 'メールアドレスが不足しています。',
|
||||
missingIDOfDocument: '更新するデータのIDが不足しています。',
|
||||
missingIDOfVersion: 'バージョンIDが不足しています。',
|
||||
|
||||
@@ -72,8 +72,6 @@ export default {
|
||||
invalidFileType: '無效的文件類型',
|
||||
invalidFileTypeValue: '無效的文件類型: {{value}}',
|
||||
loadingDocument: '加載ID為{{id}}的文件時出現了問題。',
|
||||
localesNotSaved_one: '這個語言環境無法被儲存:',
|
||||
localesNotSaved_other: '以下的語言環境無法被儲存:',
|
||||
missingEmail: '缺少電子郵件。',
|
||||
missingIDOfDocument: '缺少需要更新的文檔的ID。',
|
||||
missingIDOfVersion: '缺少版本的ID。',
|
||||
|
||||
@@ -30,12 +30,14 @@ const serverTranslationKeys = [
|
||||
'fields:chooseDocumentToLink',
|
||||
'fields:openInNewTab',
|
||||
|
||||
'general:copy',
|
||||
'general:createdAt',
|
||||
'general:deletedCountSuccessfully',
|
||||
'general:deletedSuccessfully',
|
||||
'general:email',
|
||||
'general:notFound',
|
||||
'general:successfullyCreated',
|
||||
'general:successfullyDuplicated',
|
||||
'general:thisLanguage',
|
||||
'general:user',
|
||||
'general:users',
|
||||
@@ -130,7 +132,6 @@ const clientTranslationKeys = [
|
||||
'error:correctInvalidFields',
|
||||
'error:deletingTitle',
|
||||
'error:loadingDocument',
|
||||
'error:localesNotSaved',
|
||||
'error:noMatchedField',
|
||||
'error:notAllowedToAccessPage',
|
||||
'error:previewing',
|
||||
@@ -266,7 +267,6 @@ const clientTranslationKeys = [
|
||||
'general:submit',
|
||||
'general:successfullyCreated',
|
||||
'general:successfullyDeleted',
|
||||
'general:successfullyDuplicated',
|
||||
'general:thisLanguage',
|
||||
'general:titleDeleted',
|
||||
'general:unauthorized',
|
||||
|
||||
@@ -7,11 +7,11 @@ import { toast } from 'react-toastify'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
|
||||
// import { requests } from '../../../api'
|
||||
import { useForm, useFormModified } from '../../forms/Form/context.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import * as PopupList from '../Popup/PopupButtonList/index.js'
|
||||
import './index.scss'
|
||||
@@ -21,12 +21,11 @@ const baseClass = 'duplicate'
|
||||
const Duplicate: React.FC<Props> = ({ id, slug, singularLabel }) => {
|
||||
const { Modal, useModal } = facelessUIImport
|
||||
|
||||
const { push } = useRouter()
|
||||
const router = useRouter()
|
||||
const modified = useFormModified()
|
||||
const { toggleModal } = useModal()
|
||||
const { setModified } = useForm()
|
||||
const {
|
||||
localization,
|
||||
routes: { api },
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
@@ -46,134 +45,35 @@ const Duplicate: React.FC<Props> = ({ id, slug, singularLabel }) => {
|
||||
toggleModal(modalSlug)
|
||||
return
|
||||
}
|
||||
|
||||
const saveDocument = async ({
|
||||
id,
|
||||
duplicateID = '',
|
||||
locale = '',
|
||||
}): Promise<null | string> => {
|
||||
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
|
||||
}
|
||||
|
||||
await requests.post(`${serverURL}${api}/${slug}/${id}/duplicate`, {
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/json',
|
||||
'credentials': 'include',
|
||||
},
|
||||
}).then(async (res) => {
|
||||
const { doc, message } = await res.json()
|
||||
if (res.status < 400) {
|
||||
toast.success(
|
||||
message ||
|
||||
t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }),
|
||||
{
|
||||
autoClose: 3000,
|
||||
},
|
||||
)
|
||||
|
||||
if (localeErrors.length > 0) {
|
||||
setModified(false)
|
||||
router.push(`${admin}/collections/${slug}/${doc.id}`)
|
||||
} else {
|
||||
toast.error(
|
||||
`
|
||||
${t('error:localesNotSaved', { count: localeErrors.length })}
|
||||
${localeErrors.join(', ')}
|
||||
`,
|
||||
message ||
|
||||
t('error:unspecific', { label: getTranslation(singularLabel, i18n) }),
|
||||
{ autoClose: 5000 },
|
||||
)
|
||||
}
|
||||
|
||||
setModified(false)
|
||||
|
||||
setTimeout(() => {
|
||||
push(`${admin}/collections/${slug}/${duplicateID}`)
|
||||
}, 10)
|
||||
})
|
||||
},
|
||||
[
|
||||
modified,
|
||||
localization,
|
||||
t,
|
||||
i18n,
|
||||
setModified,
|
||||
toggleModal,
|
||||
modalSlug,
|
||||
serverURL,
|
||||
api,
|
||||
slug,
|
||||
id,
|
||||
push,
|
||||
admin,
|
||||
],
|
||||
[modified, serverURL, api, slug, id, i18n, toggleModal, modalSlug, t, singularLabel, setModified, router, admin],
|
||||
)
|
||||
|
||||
const confirm = useCallback(async () => {
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
import type { BeforeDuplicate, CollectionConfig } from 'payload/types'
|
||||
import type { IndexedField } from '../../payload-types.js'
|
||||
import type {
|
||||
CollectionConfig} from 'payload/types'
|
||||
|
||||
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 = {
|
||||
slug: indexedFieldsSlug,
|
||||
// used to assert that versions also get indexes
|
||||
admin: {
|
||||
hooks: {
|
||||
beforeDuplicate,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
|
||||
@@ -619,6 +619,23 @@ describe('Fields', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user