diff --git a/src/admin/components/elements/DuplicateDocument/index.scss b/src/admin/components/elements/DuplicateDocument/index.scss index e69de29bb2..a2c98b20e6 100644 --- a/src/admin/components/elements/DuplicateDocument/index.scss +++ b/src/admin/components/elements/DuplicateDocument/index.scss @@ -0,0 +1,22 @@ +@import '../../../scss/styles.scss'; + +.duplicate { + + &__modal { + @include blur-bg; + display: flex; + align-items: center; + height: 100%; + + .btn { + margin-right: $baseline; + } + } + + &__modal-template { + z-index: 1; + position: relative; + } + + +} diff --git a/src/admin/components/elements/DuplicateDocument/index.tsx b/src/admin/components/elements/DuplicateDocument/index.tsx index 5c59984e2a..7cccfaf825 100644 --- a/src/admin/components/elements/DuplicateDocument/index.tsx +++ b/src/admin/components/elements/DuplicateDocument/index.tsx @@ -1,39 +1,143 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { useHistory } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { Modal, useModal } from '@faceless-ui/modal'; import { useConfig } from '../../utilities/Config'; import { Props } from './types'; import Button from '../Button'; -import { useForm } from '../../forms/Form/context'; +import { requests } from '../../../api'; +import { useForm, useFormModified } from '../../forms/Form/context'; +import MinimalTemplate from '../../templates/Minimal'; import './index.scss'; const baseClass = 'duplicate'; -const Duplicate: React.FC = ({ slug }) => { +const Duplicate: React.FC = ({ slug, collection, id }) => { const { push } = useHistory(); - const { getData } = useForm(); + const modified = useFormModified(); + const { toggle } = useModal(); + const { setModified } = useForm(); + const { serverURL, routes: { api }, localization } = useConfig(); const { routes: { admin } } = useConfig(); + const [hasClicked, setHasClicked] = useState(false); - const handleClick = useCallback(() => { - const data = getData(); + const modalSlug = `duplicate-${id}`; - push({ - pathname: `${admin}/collections/${slug}/create`, - state: { - data, - }, - }); - }, [push, getData, slug, admin]); + const handleClick = useCallback(async (override = false) => { + setHasClicked(true); + + if (modified && !override) { + toggle(modalSlug); + return; + } + + const create = async (locale?: string): Promise => { + const localeParam = locale ? `locale=${locale}` : ''; + const response = await requests.get(`${serverURL}${api}/${slug}/${id}?${localeParam}`); + const data = await response.json(); + const result = await requests.post(`${serverURL}${api}/${slug}`, { + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + const json = await result.json(); + + if (result.status === 201) { + return json.doc.id; + } + json.errors.forEach((error) => toast.error(error.message)); + return null; + }; + + let duplicateID; + if (localization) { + duplicateID = await create(localization.defaultLocale); + let abort = false; + localization.locales + .filter((locale) => locale !== localization.defaultLocale) + .forEach(async (locale) => { + if (!abort) { + const res = await requests.get(`${serverURL}${api}/${slug}/${id}?locale=${locale}`); + const localizedDoc = await res.json(); + const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, { + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(localizedDoc), + }); + if (patchResult.status > 400) { + abort = true; + const json = await patchResult.json(); + json.errors.forEach((error) => toast.error(error.message)); + } + } + }); + if (abort) { + // delete the duplicate doc to prevent incomplete + await requests.delete(`${serverURL}${api}/${slug}/${id}`); + } + } else { + duplicateID = await create(); + } + + toast.success(`${collection.labels.singular} successfully duplicated.`, + { autoClose: 3000 }); + + const previousModifiedState = modified; + setModified(false); + setTimeout(() => { + push({ + pathname: `${admin}/collections/${slug}/${duplicateID}`, + }); + setModified(previousModifiedState); + }, 10); + }, [modified, localization, collection.labels.singular, setModified, toggle, modalSlug, serverURL, api, slug, id, push, admin]); + + const confirm = useCallback(async () => { + setHasClicked(false); + await handleClick(true); + }, [handleClick]); return ( - + + + { modified && hasClicked && ( + + +

Confirm duplicate

+

+ You have unsaved changes. Would you like to continue to duplicate? +

+ + +
+
+ ) } +
); }; diff --git a/src/admin/components/elements/DuplicateDocument/types.ts b/src/admin/components/elements/DuplicateDocument/types.ts index ac7846166d..ef5ca4f490 100644 --- a/src/admin/components/elements/DuplicateDocument/types.ts +++ b/src/admin/components/elements/DuplicateDocument/types.ts @@ -1,3 +1,7 @@ +import { SanitizedCollectionConfig } from '../../../../collections/config/types'; + export type Props = { - slug: string, + slug: string + collection: SanitizedCollectionConfig + id: string } diff --git a/src/admin/components/views/collections/Edit/Default.tsx b/src/admin/components/views/collections/Edit/Default.tsx index 319ee9a973..94b5acf5d0 100644 --- a/src/admin/components/views/collections/Edit/Default.tsx +++ b/src/admin/components/views/collections/Edit/Default.tsx @@ -142,8 +142,14 @@ const DefaultEditView: React.FC = (props) => { Create New - {!disableDuplicate && ( -
  • + {!disableDuplicate && isEditing && ( +
  • + +
  • )} )} diff --git a/test/localization/e2e.spec.ts b/test/localization/e2e.spec.ts index df362cb798..39642e8384 100644 --- a/test/localization/e2e.spec.ts +++ b/test/localization/e2e.spec.ts @@ -1,11 +1,13 @@ import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; +import payload from '../../src'; import type { TypeWithTimestamps } from '../../src/collections/config/types'; import { AdminUrlUtil } from '../helpers/adminUrlUtil'; import { initPayloadTest } from '../helpers/configHelpers'; import { login, saveDocAndAssert } from '../helpers'; import type { LocalizedPost } from './payload-types'; import { slug } from './config'; +import { englishTitle, spanishLocale } from './shared'; /** * TODO: Localization @@ -93,6 +95,40 @@ describe('Localization', () => { await expect(page.locator('#field-description')).toHaveValue(description); }); }); + + describe('localized duplicate', () => { + let id; + + beforeAll(async () => { + const localizedPost = await payload.create({ + collection: slug, + data: { + title: englishTitle, + }, + }); + id = localizedPost.id; + await payload.update({ + collection: slug, + id, + locale: spanishLocale, + data: { + title: spanishTitle, + }, + }); + }); + + test('should duplicate data for all locales', async () => { + await page.goto(url.edit(id)); + + await page.locator('.btn.duplicate').first().click(); + await expect(page.locator('.Toastify')).toContainText('successfully'); + + await expect(page.locator('#field-title')).toHaveValue(englishTitle); + + await changeLocale(spanishLocale); + await expect(page.locator('#field-title')).toHaveValue(spanishTitle); + }); + }); }); async function fillValues(data: Partial>) {