diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index 63446b585..399bed4f1 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -167,53 +167,6 @@ those three fields plus the ID field. so your admin queries can remain performant. -### 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 = ({ 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: diff --git a/docs/hooks/fields.mdx b/docs/hooks/fields.mdx index c9772083f..675933042 100644 --- a/docs/hooks/fields.mdx +++ b/docs/hooks/fields.mdx @@ -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: diff --git a/packages/graphql/src/resolvers/collections/duplicate.ts b/packages/graphql/src/resolvers/collections/duplicate.ts new file mode 100644 index 000000000..8431beada --- /dev/null +++ b/packages/graphql/src/resolvers/collections/duplicate.ts @@ -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 = ( + _: unknown, + args: { + draft: boolean + fallbackLocale?: string + id: string + locale?: string + }, + context: { + req: PayloadRequest + }, +) => Promise + +export default function duplicateResolver( + collection: Collection, +): Resolver { + 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 + } +} diff --git a/packages/graphql/src/schema/initCollections.ts b/packages/graphql/src/schema/initCollections.ts index 8d108c468..9cc3569b0 100644 --- a/packages/graphql/src/schema/initCollections.ts +++ b/packages/graphql/src/schema/initCollections.ts @@ -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[] = [ diff --git a/packages/next/src/routes/rest/collections/duplicate.ts b/packages/next/src/routes/rest/collections/duplicate.ts new file mode 100644 index 000000000..10c9a4192 --- /dev/null +++ b/packages/next/src/routes/rest/collections/duplicate.ts @@ -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, + }, + ) +} diff --git a/packages/next/src/routes/rest/index.ts b/packages/next/src/routes/rest/index.ts index 98eab9958..48a78177a 100644 --- a/packages/next/src/routes/rest/index.ts +++ b/packages/next/src/routes/rest/index.ts @@ -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 } diff --git a/packages/payload/src/collections/config/sanitize.ts b/packages/payload/src/collections/config/sanitize.ts index 5ff47f10a..cb7e26925 100644 --- a/packages/payload/src/collections/config/sanitize.ts +++ b/packages/payload/src/collections/config/sanitize.ts @@ -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 = [] } diff --git a/packages/payload/src/collections/config/schema.ts b/packages/payload/src/collections/config/schema.ts index 650e43594..610c4583c 100644 --- a/packages/payload/src/collections/config/schema.ts +++ b/packages/payload/src/collections/config/schema.ts @@ -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({ diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index bfb5eeb4d..0bc76e44f 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -195,15 +195,6 @@ export type AfterForgotPasswordHook = (args: { context: RequestContext }) => any -type BeforeDuplicateArgs = { - /** The collection which this hook is being run on */ - collection: SanitizedCollectionConfig - data: T - locale?: string -} - -export type BeforeDuplicate = (args: BeforeDuplicateArgs) => Promise | 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 */ diff --git a/packages/payload/src/collections/operations/duplicate.ts b/packages/payload/src/collections/operations/duplicate.ts new file mode 100644 index 000000000..ae1212a2a --- /dev/null +++ b/packages/payload/src/collections/operations/duplicate.ts @@ -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 ( + incomingArgs: Arguments, +): Promise => { + 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>({ + 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({ + id, + collection: collectionConfig, + context: req.context, + data, + doc: originalDoc, + docWithLocales, + global: null, + operation, + req, + skipValidation: shouldSaveDraft, + }) + + // ///////////////////////////////////// + // Handle potential password update + // ///////////////////////////////////// + + // const dataToUpdate: Record = { ...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({ + 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({ + args, + collection: collectionConfig, + operation: 'create', + result, + }) + + // ///////////////////////////////////// + // Return results + // ///////////////////////////////////// + + if (shouldCommit) await commitTransaction(req) + + return result + } catch (error: unknown) { + await killTransaction(args.req) + throw error + } +} diff --git a/packages/payload/src/collections/operations/local/duplicate.ts b/packages/payload/src/collections/operations/local/duplicate.ts new file mode 100644 index 000000000..9a5cac881 --- /dev/null +++ b/packages/payload/src/collections/operations/local/duplicate.ts @@ -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 = { + 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( + payload: Payload, + options: Options, +): Promise { + 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({ + id, + collection, + depth, + draft, + overrideAccess, + req, + showHiddenFields, + }) +} diff --git a/packages/payload/src/collections/operations/local/index.ts b/packages/payload/src/collections/operations/local/index.ts index 57a056d89..637dbfadb 100644 --- a/packages/payload/src/collections/operations/local/index.ts +++ b/packages/payload/src/collections/operations/local/index.ts @@ -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, diff --git a/packages/payload/src/config/createClientConfig.ts b/packages/payload/src/config/createClientConfig.ts index 211cf3d19..1a487bffe 100644 --- a/packages/payload/src/config/createClientConfig.ts +++ b/packages/payload/src/config/createClientConfig.ts @@ -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 ] diff --git a/packages/payload/src/exports/operations.ts b/packages/payload/src/exports/operations.ts index abd875509..23b509a03 100644 --- a/packages/payload/src/exports/operations.ts +++ b/packages/payload/src/exports/operations.ts @@ -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' diff --git a/packages/payload/src/exports/types.ts b/packages/payload/src/exports/types.ts index 67a045561..6f296c7ce 100644 --- a/packages/payload/src/exports/types.ts +++ b/packages/payload/src/exports/types.ts @@ -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, diff --git a/packages/payload/src/fields/config/sanitize.ts b/packages/payload/src/fields/config/sanitize.ts index 22cae6161..6d49b6dd9 100644 --- a/packages/payload/src/fields/config/sanitize.ts +++ b/packages/payload/src/fields/config/sanitize.ts @@ -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) { diff --git a/packages/payload/src/fields/config/schema.ts b/packages/payload/src/fields/config/schema.ts index 461663818..cc8a1cc32 100644 --- a/packages/payload/src/fields/config/schema.ts +++ b/packages/payload/src/fields/config/schema.ts @@ -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(), diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index ea34aa2da..a725de9b5 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -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 diff --git a/packages/payload/src/fields/hooks/beforeValidate/index.ts b/packages/payload/src/fields/hooks/beforeValidate/index.ts index 21b2c55b3..9f81582d5 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/index.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/index.ts @@ -10,6 +10,7 @@ type Args = { context: RequestContext data: Record | T doc?: Record | T + duplicate?: boolean global: SanitizedGlobalConfig | null id?: number | string operation: 'create' | 'update' @@ -23,6 +24,7 @@ export const beforeValidate = async >({ context, data: incomingData, doc, + duplicate = false, global, operation, overrideAccess, @@ -36,6 +38,7 @@ export const beforeValidate = async >({ context, data, doc, + duplicate, fields: collection?.fields || global?.fields, global, operation, diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index cca85870a..480d317f7 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -15,6 +15,7 @@ type Args = { 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 ({ context, data, doc, + duplicate, field, global, operation, @@ -259,6 +261,32 @@ export const promise = async ({ }) } } + + // 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 ({ context, data, doc, + duplicate, fields: field.fields, global, operation, @@ -301,6 +330,7 @@ export const promise = async ({ context, data, doc, + duplicate, fields: field.fields, global, operation, @@ -336,6 +366,7 @@ export const promise = async ({ context, data, doc, + duplicate, fields: block.fields, global, operation, @@ -361,6 +392,7 @@ export const promise = async ({ context, data, doc, + duplicate, fields: field.fields, global, operation, @@ -393,6 +425,7 @@ export const promise = async ({ context, data, doc, + duplicate, fields: field.fields, global, operation, @@ -412,6 +445,7 @@ export const promise = async ({ context, data, doc, + duplicate, fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), global, operation, diff --git a/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts b/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts index 2367673f8..81d6a0939 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts @@ -10,6 +10,7 @@ type Args = { 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 ({ context, data, doc, + duplicate, fields, global, operation, @@ -43,6 +45,7 @@ export const traverseFields = async ({ context, data, doc, + duplicate, field, global, operation, diff --git a/packages/payload/src/fields/setDefaultBeforeDuplicate.ts b/packages/payload/src/fields/setDefaultBeforeDuplicate.ts new file mode 100644 index 000000000..63bdb646b --- /dev/null +++ b/packages/payload/src/fields/setDefaultBeforeDuplicate.ts @@ -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 + ] + } + } +} diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 3f28b8987..32ddf0af3 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -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 { const { update } = localOperations return update(this, options) } + + duplicate = async ( + options: DuplicateOptions, + ): Promise => { + const { duplicate } = localOperations + return duplicate(this, options) + } } const initialized = new BasePayload() diff --git a/packages/translations/src/_generatedFiles_/api/ar.js b/packages/translations/src/_generatedFiles_/api/ar.js index 36e3f29c1..f382a4cbc 100644 --- a/packages/translations/src/_generatedFiles_/api/ar.js +++ b/packages/translations/src/_generatedFiles_/api/ar.js @@ -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}} بنجاح.', diff --git a/packages/translations/src/_generatedFiles_/api/az.js b/packages/translations/src/_generatedFiles_/api/az.js index 537b339ea..749f00704 100644 --- a/packages/translations/src/_generatedFiles_/api/az.js +++ b/packages/translations/src/_generatedFiles_/api/az.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/bg.js b/packages/translations/src/_generatedFiles_/api/bg.js index f3dbed21f..ff7816902 100644 --- a/packages/translations/src/_generatedFiles_/api/bg.js +++ b/packages/translations/src/_generatedFiles_/api/bg.js @@ -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}} успешно.', diff --git a/packages/translations/src/_generatedFiles_/api/cs.js b/packages/translations/src/_generatedFiles_/api/cs.js index c30e19ffe..2c028b7cb 100644 --- a/packages/translations/src/_generatedFiles_/api/cs.js +++ b/packages/translations/src/_generatedFiles_/api/cs.js @@ -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}}.', diff --git a/packages/translations/src/_generatedFiles_/api/de.js b/packages/translations/src/_generatedFiles_/api/de.js index 2a39528e8..7338b4cc2 100644 --- a/packages/translations/src/_generatedFiles_/api/de.js +++ b/packages/translations/src/_generatedFiles_/api/de.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/en.js b/packages/translations/src/_generatedFiles_/api/en.js index 67a498b3c..70cd36f04 100644 --- a/packages/translations/src/_generatedFiles_/api/en.js +++ b/packages/translations/src/_generatedFiles_/api/en.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/es.js b/packages/translations/src/_generatedFiles_/api/es.js index 3001ba250..d2270bf52 100644 --- a/packages/translations/src/_generatedFiles_/api/es.js +++ b/packages/translations/src/_generatedFiles_/api/es.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/fa.js b/packages/translations/src/_generatedFiles_/api/fa.js index 1a353928a..f0caa9b1d 100644 --- a/packages/translations/src/_generatedFiles_/api/fa.js +++ b/packages/translations/src/_generatedFiles_/api/fa.js @@ -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}} با موفقیت بروزرسانی شدند.', diff --git a/packages/translations/src/_generatedFiles_/api/fr.js b/packages/translations/src/_generatedFiles_/api/fr.js index b07278818..1dbd1c811 100644 --- a/packages/translations/src/_generatedFiles_/api/fr.js +++ b/packages/translations/src/_generatedFiles_/api/fr.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/hr.js b/packages/translations/src/_generatedFiles_/api/hr.js index b3b37c8b1..869096b27 100644 --- a/packages/translations/src/_generatedFiles_/api/hr.js +++ b/packages/translations/src/_generatedFiles_/api/hr.js @@ -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}}.', diff --git a/packages/translations/src/_generatedFiles_/api/hu.js b/packages/translations/src/_generatedFiles_/api/hu.js index 11ecb3788..21a8603d9 100644 --- a/packages/translations/src/_generatedFiles_/api/hu.js +++ b/packages/translations/src/_generatedFiles_/api/hu.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/it.js b/packages/translations/src/_generatedFiles_/api/it.js index 34b1ccaae..8f1dba997 100644 --- a/packages/translations/src/_generatedFiles_/api/it.js +++ b/packages/translations/src/_generatedFiles_/api/it.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/ja.js b/packages/translations/src/_generatedFiles_/api/ja.js index 1ee833a20..e3a6f548b 100644 --- a/packages/translations/src/_generatedFiles_/api/ja.js +++ b/packages/translations/src/_generatedFiles_/api/ja.js @@ -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}}を正常に更新しました。', diff --git a/packages/translations/src/_generatedFiles_/api/ko.js b/packages/translations/src/_generatedFiles_/api/ko.js index 920e9f91a..5fbd33f26 100644 --- a/packages/translations/src/_generatedFiles_/api/ko.js +++ b/packages/translations/src/_generatedFiles_/api/ko.js @@ -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}}을(를) 업데이트했습니다.', diff --git a/packages/translations/src/_generatedFiles_/api/my.js b/packages/translations/src/_generatedFiles_/api/my.js index 204ec71fb..1f3cd22eb 100644 --- a/packages/translations/src/_generatedFiles_/api/my.js +++ b/packages/translations/src/_generatedFiles_/api/my.js @@ -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}} ကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ခဲ့သည်။', diff --git a/packages/translations/src/_generatedFiles_/api/nb.js b/packages/translations/src/_generatedFiles_/api/nb.js index 1bba672f5..424dfeff2 100644 --- a/packages/translations/src/_generatedFiles_/api/nb.js +++ b/packages/translations/src/_generatedFiles_/api/nb.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/nl.js b/packages/translations/src/_generatedFiles_/api/nl.js index d10f4faac..31ac2bcf6 100644 --- a/packages/translations/src/_generatedFiles_/api/nl.js +++ b/packages/translations/src/_generatedFiles_/api/nl.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/pl.js b/packages/translations/src/_generatedFiles_/api/pl.js index fa86d6704..44c8b0d81 100644 --- a/packages/translations/src/_generatedFiles_/api/pl.js +++ b/packages/translations/src/_generatedFiles_/api/pl.js @@ -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}}.', diff --git a/packages/translations/src/_generatedFiles_/api/pt.js b/packages/translations/src/_generatedFiles_/api/pt.js index ebb23da09..53cf7d3d3 100644 --- a/packages/translations/src/_generatedFiles_/api/pt.js +++ b/packages/translations/src/_generatedFiles_/api/pt.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/ro.js b/packages/translations/src/_generatedFiles_/api/ro.js index e0c9eb8f3..e14d76793 100644 --- a/packages/translations/src/_generatedFiles_/api/ro.js +++ b/packages/translations/src/_generatedFiles_/api/ro.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/rs-latin.js b/packages/translations/src/_generatedFiles_/api/rs-latin.js index 9eda1a00d..bf519d6b1 100644 --- a/packages/translations/src/_generatedFiles_/api/rs-latin.js +++ b/packages/translations/src/_generatedFiles_/api/rs-latin.js @@ -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}}.', diff --git a/packages/translations/src/_generatedFiles_/api/rs.js b/packages/translations/src/_generatedFiles_/api/rs.js index 25d663af2..6c7495e58 100644 --- a/packages/translations/src/_generatedFiles_/api/rs.js +++ b/packages/translations/src/_generatedFiles_/api/rs.js @@ -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}}.', diff --git a/packages/translations/src/_generatedFiles_/api/ru.js b/packages/translations/src/_generatedFiles_/api/ru.js index abc96d78e..bcb695f53 100644 --- a/packages/translations/src/_generatedFiles_/api/ru.js +++ b/packages/translations/src/_generatedFiles_/api/ru.js @@ -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}} успешно.', diff --git a/packages/translations/src/_generatedFiles_/api/sv.js b/packages/translations/src/_generatedFiles_/api/sv.js index c62b51e16..0c7c44c89 100644 --- a/packages/translations/src/_generatedFiles_/api/sv.js +++ b/packages/translations/src/_generatedFiles_/api/sv.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/th.js b/packages/translations/src/_generatedFiles_/api/th.js index da1674526..f85be9c16 100644 --- a/packages/translations/src/_generatedFiles_/api/th.js +++ b/packages/translations/src/_generatedFiles_/api/th.js @@ -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}} เรียบร้อยแล้ว', diff --git a/packages/translations/src/_generatedFiles_/api/tr.js b/packages/translations/src/_generatedFiles_/api/tr.js index 618b5b780..0b8f6e58b 100644 --- a/packages/translations/src/_generatedFiles_/api/tr.js +++ b/packages/translations/src/_generatedFiles_/api/tr.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/api/ua.js b/packages/translations/src/_generatedFiles_/api/ua.js index 0aece31a9..1691af1cb 100644 --- a/packages/translations/src/_generatedFiles_/api/ua.js +++ b/packages/translations/src/_generatedFiles_/api/ua.js @@ -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}}.', diff --git a/packages/translations/src/_generatedFiles_/api/vi.js b/packages/translations/src/_generatedFiles_/api/vi.js index 4a253c212..e81ef1a25 100644 --- a/packages/translations/src/_generatedFiles_/api/vi.js +++ b/packages/translations/src/_generatedFiles_/api/vi.js @@ -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}}.', diff --git a/packages/translations/src/_generatedFiles_/api/zh-tw.js b/packages/translations/src/_generatedFiles_/api/zh-tw.js index ed560b104..070177df6 100644 --- a/packages/translations/src/_generatedFiles_/api/zh-tw.js +++ b/packages/translations/src/_generatedFiles_/api/zh-tw.js @@ -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}}。', diff --git a/packages/translations/src/_generatedFiles_/api/zh.js b/packages/translations/src/_generatedFiles_/api/zh.js index 28091066e..fcb545e5c 100644 --- a/packages/translations/src/_generatedFiles_/api/zh.js +++ b/packages/translations/src/_generatedFiles_/api/zh.js @@ -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}}。', diff --git a/packages/translations/src/_generatedFiles_/client/ar.js b/packages/translations/src/_generatedFiles_/client/ar.js index 40119e9dd..188ef07e7 100644 --- a/packages/translations/src/_generatedFiles_/client/ar.js +++ b/packages/translations/src/_generatedFiles_/client/ar.js @@ -188,7 +188,6 @@ export default { submissionSuccessful: 'تمت الإرسال بنجاح.', submit: 'إرسال', successfullyCreated: '{{label}} تم إنشاؤها بنجاح.', - successfullyDuplicated: '{{label}} تم استنساخها بنجاح.', thisLanguage: 'العربية', titleDeleted: 'تم حذف {{label}} "{{title}}" بنجاح.', unauthorized: 'غير مصرح به', diff --git a/packages/translations/src/_generatedFiles_/client/az.js b/packages/translations/src/_generatedFiles_/client/az.js index bfa9c47d7..289142eb3 100644 --- a/packages/translations/src/_generatedFiles_/client/az.js +++ b/packages/translations/src/_generatedFiles_/client/az.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/bg.js b/packages/translations/src/_generatedFiles_/client/bg.js index d962f3e67..6aff47959 100644 --- a/packages/translations/src/_generatedFiles_/client/bg.js +++ b/packages/translations/src/_generatedFiles_/client/bg.js @@ -188,7 +188,6 @@ export default { submissionSuccessful: 'Успешно подаване.', submit: 'Подай', successfullyCreated: '{{label}} успешно създаден.', - successfullyDuplicated: '{{label}} успешно дупликиран.', thisLanguage: 'Български', titleDeleted: '{{label}} "{{title}}" успешно изтрит.', unauthorized: 'Неавторизиран', diff --git a/packages/translations/src/_generatedFiles_/client/cs.js b/packages/translations/src/_generatedFiles_/client/cs.js index c4c95a410..f299c35eb 100644 --- a/packages/translations/src/_generatedFiles_/client/cs.js +++ b/packages/translations/src/_generatedFiles_/client/cs.js @@ -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ý', diff --git a/packages/translations/src/_generatedFiles_/client/de.js b/packages/translations/src/_generatedFiles_/client/de.js index b9aaa6aa5..c577e523d 100644 --- a/packages/translations/src/_generatedFiles_/client/de.js +++ b/packages/translations/src/_generatedFiles_/client/de.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/en.js b/packages/translations/src/_generatedFiles_/client/en.js index f840a257c..89e0e2b04 100644 --- a/packages/translations/src/_generatedFiles_/client/en.js +++ b/packages/translations/src/_generatedFiles_/client/en.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/es.js b/packages/translations/src/_generatedFiles_/client/es.js index 4f9f8b946..a0a00913b 100644 --- a/packages/translations/src/_generatedFiles_/client/es.js +++ b/packages/translations/src/_generatedFiles_/client/es.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/fa.js b/packages/translations/src/_generatedFiles_/client/fa.js index 0266171d7..c778583df 100644 --- a/packages/translations/src/_generatedFiles_/client/fa.js +++ b/packages/translations/src/_generatedFiles_/client/fa.js @@ -188,7 +188,6 @@ export default { submissionSuccessful: 'با موفقیت ثبت شد.', submit: 'فرستادن', successfullyCreated: '{{label}} با موفقیت ساخته شد.', - successfullyDuplicated: '{{label}} با موفقیت رونوشت شد.', thisLanguage: 'فارسی', titleDeleted: '{{label}} "{{title}}" با موفقیت پاک شد.', unauthorized: 'غیرمجاز', diff --git a/packages/translations/src/_generatedFiles_/client/fr.js b/packages/translations/src/_generatedFiles_/client/fr.js index 3a4523f52..ee86feed6 100644 --- a/packages/translations/src/_generatedFiles_/client/fr.js +++ b/packages/translations/src/_generatedFiles_/client/fr.js @@ -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é', diff --git a/packages/translations/src/_generatedFiles_/client/hr.js b/packages/translations/src/_generatedFiles_/client/hr.js index 1d708fcfc..2ea2e6b9a 100644 --- a/packages/translations/src/_generatedFiles_/client/hr.js +++ b/packages/translations/src/_generatedFiles_/client/hr.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/hu.js b/packages/translations/src/_generatedFiles_/client/hu.js index 03e491882..2629f7801 100644 --- a/packages/translations/src/_generatedFiles_/client/hu.js +++ b/packages/translations/src/_generatedFiles_/client/hu.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/it.js b/packages/translations/src/_generatedFiles_/client/it.js index 067be3158..2bf39dd10 100644 --- a/packages/translations/src/_generatedFiles_/client/it.js +++ b/packages/translations/src/_generatedFiles_/client/it.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/ja.js b/packages/translations/src/_generatedFiles_/client/ja.js index 8fd47f2ca..e3ba87cca 100644 --- a/packages/translations/src/_generatedFiles_/client/ja.js +++ b/packages/translations/src/_generatedFiles_/client/ja.js @@ -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: '未認証', diff --git a/packages/translations/src/_generatedFiles_/client/ko.js b/packages/translations/src/_generatedFiles_/client/ko.js index 7536af805..6dc1f31d1 100644 --- a/packages/translations/src/_generatedFiles_/client/ko.js +++ b/packages/translations/src/_generatedFiles_/client/ko.js @@ -188,7 +188,6 @@ export default { submissionSuccessful: '제출이 완료되었습니다.', submit: '제출', successfullyCreated: '{{label}}이(가) 생성되었습니다.', - successfullyDuplicated: '{{label}}이(가) 복제되었습니다.', thisLanguage: '한국어', titleDeleted: '{{label}} "{{title}}"을(를) 삭제했습니다.', unauthorized: '권한 없음', diff --git a/packages/translations/src/_generatedFiles_/client/my.js b/packages/translations/src/_generatedFiles_/client/my.js index 25654d83f..a8973b4d3 100644 --- a/packages/translations/src/_generatedFiles_/client/my.js +++ b/packages/translations/src/_generatedFiles_/client/my.js @@ -190,7 +190,6 @@ export default { submissionSuccessful: 'သိမ်းဆည်းမှု အောင်မြင်ပါသည်။', submit: 'သိမ်းဆည်းမည်။', successfullyCreated: '{{label}} အောင်မြင်စွာဖန်တီးခဲ့သည်။', - successfullyDuplicated: '{{label}} အောင်မြင်စွာ ပုံတူပွားခဲ့သည်။', thisLanguage: 'မြန်မာစာ', titleDeleted: '{{label}} {{title}} အောင်မြင်စွာ ဖျက်သိမ်းခဲ့သည်။', unauthorized: 'အခွင့်မရှိပါ။', diff --git a/packages/translations/src/_generatedFiles_/client/nb.js b/packages/translations/src/_generatedFiles_/client/nb.js index b77df0f4c..6fc8fea43 100644 --- a/packages/translations/src/_generatedFiles_/client/nb.js +++ b/packages/translations/src/_generatedFiles_/client/nb.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/nl.js b/packages/translations/src/_generatedFiles_/client/nl.js index 48362a60f..cb16b1bab 100644 --- a/packages/translations/src/_generatedFiles_/client/nl.js +++ b/packages/translations/src/_generatedFiles_/client/nl.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/pl.js b/packages/translations/src/_generatedFiles_/client/pl.js index d2bd06c8c..55f01ab3f 100644 --- a/packages/translations/src/_generatedFiles_/client/pl.js +++ b/packages/translations/src/_generatedFiles_/client/pl.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/pt.js b/packages/translations/src/_generatedFiles_/client/pt.js index 4ffff92bc..61029d823 100644 --- a/packages/translations/src/_generatedFiles_/client/pt.js +++ b/packages/translations/src/_generatedFiles_/client/pt.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/ro.js b/packages/translations/src/_generatedFiles_/client/ro.js index e8aaceacb..b306484ce 100644 --- a/packages/translations/src/_generatedFiles_/client/ro.js +++ b/packages/translations/src/_generatedFiles_/client/ro.js @@ -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(ă)', diff --git a/packages/translations/src/_generatedFiles_/client/rs-latin.js b/packages/translations/src/_generatedFiles_/client/rs-latin.js index 4e7a6ff9b..c06d1e760 100644 --- a/packages/translations/src/_generatedFiles_/client/rs-latin.js +++ b/packages/translations/src/_generatedFiles_/client/rs-latin.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/rs.js b/packages/translations/src/_generatedFiles_/client/rs.js index 04e7ec099..bb4d7dac5 100644 --- a/packages/translations/src/_generatedFiles_/client/rs.js +++ b/packages/translations/src/_generatedFiles_/client/rs.js @@ -188,7 +188,6 @@ export default { submissionSuccessful: 'Успешно слање', submit: 'Потврди', successfullyCreated: '{{label}} успешно креирано.', - successfullyDuplicated: '{{label}} успешно дуплицирано.', thisLanguage: 'Српски (ћирилица)', titleDeleted: '{{label}} "{{title}}" успешно обрисано.', unauthorized: 'Нисте ауторизовани', diff --git a/packages/translations/src/_generatedFiles_/client/ru.js b/packages/translations/src/_generatedFiles_/client/ru.js index aeb479726..0c6f638d2 100644 --- a/packages/translations/src/_generatedFiles_/client/ru.js +++ b/packages/translations/src/_generatedFiles_/client/ru.js @@ -189,7 +189,6 @@ export default { submissionSuccessful: 'Успешно отправлено.', submit: 'Отправить', successfullyCreated: '{{label}} успешно создан.', - successfullyDuplicated: '{{label}} успешно продублирован.', thisLanguage: 'Русский', titleDeleted: '{{label}} {{title}} успешно удалено.', unauthorized: 'Нет доступа', diff --git a/packages/translations/src/_generatedFiles_/client/sv.js b/packages/translations/src/_generatedFiles_/client/sv.js index 80355305d..6825ebfd7 100644 --- a/packages/translations/src/_generatedFiles_/client/sv.js +++ b/packages/translations/src/_generatedFiles_/client/sv.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/th.js b/packages/translations/src/_generatedFiles_/client/th.js index b96571f36..83d358da7 100644 --- a/packages/translations/src/_generatedFiles_/client/th.js +++ b/packages/translations/src/_generatedFiles_/client/th.js @@ -187,7 +187,6 @@ export default { submissionSuccessful: 'ส่งสำเร็จ', submit: 'ส่ง', successfullyCreated: 'สร้าง {{label}} สำเร็จ', - successfullyDuplicated: 'สำเนา {{label}} สำเร็จ', thisLanguage: 'ไทย', titleDeleted: 'ลบ {{label}} "{{title}}" สำเร็จ', unauthorized: 'ไม่ได้รับอนุญาต', diff --git a/packages/translations/src/_generatedFiles_/client/tr.js b/packages/translations/src/_generatedFiles_/client/tr.js index 7ab28f94b..77f5b3f84 100644 --- a/packages/translations/src/_generatedFiles_/client/tr.js +++ b/packages/translations/src/_generatedFiles_/client/tr.js @@ -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', diff --git a/packages/translations/src/_generatedFiles_/client/ua.js b/packages/translations/src/_generatedFiles_/client/ua.js index 3a2a4c97c..2b2bf584c 100644 --- a/packages/translations/src/_generatedFiles_/client/ua.js +++ b/packages/translations/src/_generatedFiles_/client/ua.js @@ -188,7 +188,6 @@ export default { submissionSuccessful: 'Успішно відправлено.', submit: 'Відправити', successfullyCreated: '{{label}} успішно створено.', - successfullyDuplicated: '{{label}} успішно продубльовано.', thisLanguage: 'Українська', titleDeleted: '{{label}} "{{title}}" успішно видалено.', unauthorized: 'Немає доступу', diff --git a/packages/translations/src/_generatedFiles_/client/vi.js b/packages/translations/src/_generatedFiles_/client/vi.js index d6a8b4be9..1fbecb2ce 100644 --- a/packages/translations/src/_generatedFiles_/client/vi.js +++ b/packages/translations/src/_generatedFiles_/client/vi.js @@ -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.', diff --git a/packages/translations/src/_generatedFiles_/client/zh-tw.js b/packages/translations/src/_generatedFiles_/client/zh-tw.js index 3b5d1545a..eb6e36845 100644 --- a/packages/translations/src/_generatedFiles_/client/zh-tw.js +++ b/packages/translations/src/_generatedFiles_/client/zh-tw.js @@ -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: '未經授權', diff --git a/packages/translations/src/_generatedFiles_/client/zh.js b/packages/translations/src/_generatedFiles_/client/zh.js index 79cc3f1e5..c39e221d7 100644 --- a/packages/translations/src/_generatedFiles_/client/zh.js +++ b/packages/translations/src/_generatedFiles_/client/zh.js @@ -186,7 +186,6 @@ export default { submissionSuccessful: '提交成功。', submit: '提交', successfullyCreated: '成功创建{{label}}', - successfullyDuplicated: '成功复制{{label}}', thisLanguage: '中文 (简体)', titleDeleted: '{{label}} "{{title}}"已被成功删除。', unauthorized: '未经授权', diff --git a/packages/translations/src/all/en.ts b/packages/translations/src/all/en.ts index 63f933990..d2d674038 100644 --- a/packages/translations/src/all/en.ts +++ b/packages/translations/src/all/en.ts @@ -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.', diff --git a/packages/translations/src/all/ja.ts b/packages/translations/src/all/ja.ts index 291561295..864e6a3cd 100644 --- a/packages/translations/src/all/ja.ts +++ b/packages/translations/src/all/ja.ts @@ -78,8 +78,6 @@ export default { invalidFileType: '無効なファイル形式', invalidFileTypeValue: '無効なファイル形式: {{value}}', loadingDocument: 'IDが {{id}} のデータを読み込む際に問題が発生しました。', - localesNotSaved_one: '次のロケールは保存できませんでした:', - localesNotSaved_other: '次のロケールは保存できませんでした:', missingEmail: 'メールアドレスが不足しています。', missingIDOfDocument: '更新するデータのIDが不足しています。', missingIDOfVersion: 'バージョンIDが不足しています。', diff --git a/packages/translations/src/all/zh-tw.ts b/packages/translations/src/all/zh-tw.ts index cc8144a1d..7afb6f1f9 100644 --- a/packages/translations/src/all/zh-tw.ts +++ b/packages/translations/src/all/zh-tw.ts @@ -72,8 +72,6 @@ export default { invalidFileType: '無效的文件類型', invalidFileTypeValue: '無效的文件類型: {{value}}', loadingDocument: '加載ID為{{id}}的文件時出現了問題。', - localesNotSaved_one: '這個語言環境無法被儲存:', - localesNotSaved_other: '以下的語言環境無法被儲存:', missingEmail: '缺少電子郵件。', missingIDOfDocument: '缺少需要更新的文檔的ID。', missingIDOfVersion: '缺少版本的ID。', diff --git a/packages/translations/writeTranslationFiles.ts b/packages/translations/writeTranslationFiles.ts index a29041ac6..a8969ba8b 100644 --- a/packages/translations/writeTranslationFiles.ts +++ b/packages/translations/writeTranslationFiles.ts @@ -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', diff --git a/packages/ui/src/elements/DuplicateDocument/index.tsx b/packages/ui/src/elements/DuplicateDocument/index.tsx index 6a8e98a20..ee8aa7f74 100644 --- a/packages/ui/src/elements/DuplicateDocument/index.tsx +++ b/packages/ui/src/elements/DuplicateDocument/index.tsx @@ -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 = ({ 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 = ({ id, slug, singularLabel }) => { toggleModal(modalSlug) return } - - const saveDocument = async ({ - id, - duplicateID = '', - locale = '', - }): Promise => { - 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, + await requests.post(`${serverURL}${api}/${slug}/${id}/duplicate`, { + body: JSON.stringify({}), + headers: { + 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', + 'credentials': 'include', }, - ) - - if (localeErrors.length > 0) { - toast.error( - ` - ${t('error:localesNotSaved', { count: localeErrors.length })} - ${localeErrors.join(', ')} - `, - { autoClose: 5000 }, - ) - } - - setModified(false) - - setTimeout(() => { - push(`${admin}/collections/${slug}/${duplicateID}`) - }, 10) + }).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, + }, + ) + setModified(false) + router.push(`${admin}/collections/${slug}/${doc.id}`) + } else { + toast.error( + message || + t('error:unspecific', { label: getTranslation(singularLabel, i18n) }), + { autoClose: 5000 }, + ) + } + }) }, - [ - 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 () => { diff --git a/test/fields/collections/Indexed/index.ts b/test/fields/collections/Indexed/index.ts index 1c3a3744b..ae7c44583 100644 --- a/test/fields/collections/Indexed/index.ts +++ b/test/fields/collections/Indexed/index.ts @@ -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 = ({ 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', diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index c9ef28a84..8490f6f5c 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -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', () => {