diff --git a/extension/src/editor/containers/common/commonTemplate/TemplateHeader.tsx b/extension/src/editor/containers/common/commonTemplate/TemplateHeader.tsx index e262c6518..1e15b0e2a 100644 --- a/extension/src/editor/containers/common/commonTemplate/TemplateHeader.tsx +++ b/extension/src/editor/containers/common/commonTemplate/TemplateHeader.tsx @@ -17,6 +17,7 @@ type TemplateHeaderProps = { templateNames: string[]; onTemplateRename?: (templateName: string) => void; onTemplateRemove?: (templateId: string) => void; + onTemplateDuplicate?: (templateName: string) => void; }; export const TemplateHeader: React.FC = ({ @@ -25,6 +26,7 @@ export const TemplateHeader: React.FC = ({ templateNames, onTemplateRename, onTemplateRemove, + onTemplateDuplicate, }) => { const [menuOpen, setMenuOpen] = useState(false); const anchorRef = useRef(null); @@ -85,6 +87,38 @@ export const TemplateHeader: React.FC = ({ setRenameTemplateOpen(false); }, [newTemplateName, onTemplateRename]); + const [duplicateTemplateOpen, setDuplicateTemplateOpen] = useState(false); + const handleOpenDuplicateTemplate = useCallback(() => { + setDuplicateTemplateOpen(true); + setMenuOpen(false); + }, []); + const handleCloseDuplicateTemplate = useCallback(() => { + setDuplicateTemplateOpen(false); + setDuplicatedTemplateName(generateDuplicateTemplateName(templateName)); + }, [templateName]); + + const [duplicatedTemplateName, setDuplicatedTemplateName] = useState( + generateDuplicateTemplateName(templateName), + ); + const handleDuplicatedTemplateNameChange = useCallback( + (e: React.ChangeEvent) => { + setDuplicatedTemplateName(e.target.value); + }, + [], + ); + const duplicatedTemplateNameValid = useMemo(() => { + return ( + templateNames.includes(duplicatedTemplateName) || + duplicatedTemplateName === "" || + duplicatedTemplateName.endsWith("/") || + duplicatedTemplateName.split("/").some(p => p === "") + ); + }, [duplicatedTemplateName, templateNames]); + const handleTemplateDuplicate = useCallback(() => { + onTemplateDuplicate?.(duplicatedTemplateName); + setDuplicateTemplateOpen(false); + }, [duplicatedTemplateName, onTemplateDuplicate]); + return ( @@ -102,6 +136,9 @@ export const TemplateHeader: React.FC = ({ Rename + + Duplicate + Delete @@ -138,6 +175,22 @@ export const TemplateHeader: React.FC = ({ onChange={handleNewTemplateNameChange} /> + + + ); @@ -167,3 +220,7 @@ const StyledButton = styled(Button)(({ theme }) => ({ borderRadius: "0", }, })); + +function generateDuplicateTemplateName(ori: string) { + return `${ori} (copy-${new Date().getTime()})`; +} diff --git a/extension/src/editor/containers/component-template/index.tsx b/extension/src/editor/containers/component-template/index.tsx index 242c46d0d..b80a098c7 100644 --- a/extension/src/editor/containers/component-template/index.tsx +++ b/extension/src/editor/containers/component-template/index.tsx @@ -197,6 +197,43 @@ export const EditorFieldComponentsTemplateSection: React.FC< [editorNoticeRef, removeTemplate], ); + const handleTemplateDuplicate = useCallback( + async (newTemplateName: string) => { + if (!template) return; + const newTemplate = { + name: newTemplateName, + type: "component", + groups: template.groups.map(g => ({ + ...g, + id: generateID(), + components: g.components.map(c => ({ + ...c, + id: generateID(), + })), + })), + } as unknown as ComponentTemplate; + + setIsSaving(true); + await saveTemplate(newTemplate) + .then(() => { + editorNoticeRef?.current?.show({ + severity: "success", + message: "Template duplicated!", + }); + }) + .catch(() => { + editorNoticeRef?.current?.show({ + severity: "error", + message: "Template duplicate failed!", + }); + }) + .finally(() => { + setIsSaving(false); + }); + }, + [editorNoticeRef, saveTemplate, template], + ); + return ( ) } diff --git a/extension/src/editor/containers/emphasis-property-template/index.tsx b/extension/src/editor/containers/emphasis-property-template/index.tsx index 301cea5ce..1adcf4da9 100644 --- a/extension/src/editor/containers/emphasis-property-template/index.tsx +++ b/extension/src/editor/containers/emphasis-property-template/index.tsx @@ -2,6 +2,7 @@ import { useMemo, useState, useCallback, useEffect } from "react"; import { useTemplateAPI } from "../../../shared/api"; import { EmphasisPropertyTemplate } from "../../../shared/api/types"; +import { generateID } from "../../../shared/utils"; import { TemplateAddButton } from "../common/commonTemplate/TemplateAddButton"; import { TemplateHeader } from "../common/commonTemplate/TemplateHeader"; import { EditorSection, EditorTree, EditorTreeSelection } from "../ui-components"; @@ -188,6 +189,36 @@ export const EditorInspectorEmphasisPropertyTemplateSection: React.FC< [editorNoticeRef, removeTemplate], ); + const handleTemplateDuplicate = useCallback( + async (newTemplateName: string) => { + if (!template) return; + const newTemplate = { + name: newTemplateName, + type: "emphasis", + properties: template.properties.map(p => ({ ...p, id: generateID() })), + } as unknown as EmphasisPropertyTemplate; + + setIsSaving(true); + await saveTemplate(newTemplate) + .then(() => { + editorNoticeRef?.current?.show({ + severity: "success", + message: "Template duplicated!", + }); + }) + .catch(() => { + editorNoticeRef?.current?.show({ + severity: "error", + message: "Template duplicate failed!", + }); + }) + .finally(() => { + setIsSaving(false); + }); + }, + [editorNoticeRef, saveTemplate, template], + ); + return ( ) }