diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index f7810d3f..322c63df 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -113,7 +113,8 @@ "invalid_file_type": "Invalid file type. Please select a file in the supported format.", "select_category": "Select a flow", "logout_failed": "Something went wrong during logout", - "duplicate_labels_not_allowed": "Duplicate labels are not allowed" + "duplicate_labels_not_allowed": "Duplicate labels are not allowed", + "duplicate_block_error": "Something went wrong while duplicating block" }, "menu": { "terms": "Terms of Use", diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index e257e4fd..fa45a278 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -113,7 +113,8 @@ "invalid_file_type": "Type de fichier invalide. Veuillez choisir un fichier dans un format pris en charge.", "select_category": "Sélectionner une catégorie", "logout_failed": "Une erreur s'est produite lors de la déconnexion", - "duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées" + "duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées", + "duplicate_block_error": "Une erreur est survenue lors de la duplication du bloc" }, "menu": { "terms": "Conditions d'utilisation", diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 924cb9ba..dad61fd5 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -1,12 +1,13 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Add, MoveUp } from "@mui/icons-material"; + +import { Add, ContentCopyRounded, MoveUp } from "@mui/icons-material"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import FitScreenIcon from "@mui/icons-material/FitScreen"; @@ -28,6 +29,7 @@ import { DiagramEngine, DiagramModel, DiagramModelGenerics, + NodeModel, } from "@projectstorm/react-diagrams"; import { useRouter } from "next/router"; import { SyntheticEvent, useCallback, useEffect, useState } from "react"; @@ -37,6 +39,7 @@ import { ConfirmDialogBody } from "@/app-components/dialogs"; import { CategoryFormDialog } from "@/components/categories/CategoryFormDialog"; import { BlockMoveFormDialog } from "@/components/visual-editor/BlockMoveFormDialog"; import { isSameEntity } from "@/hooks/crud/helpers"; +import { useCreate } from "@/hooks/crud/useCreate"; import { useDeleteFromCache } from "@/hooks/crud/useDelete"; import { useDeleteMany } from "@/hooks/crud/useDeleteMany"; import { useFind } from "@/hooks/crud/useFind"; @@ -46,9 +49,10 @@ import { useUpdateMany } from "@/hooks/crud/useUpdateMany"; import useDebouncedUpdate from "@/hooks/useDebouncedUpdate"; import { useDialogs } from "@/hooks/useDialogs"; import { useSearch } from "@/hooks/useSearch"; +import { useToast } from "@/hooks/useToast"; import { useTranslate } from "@/hooks/useTranslate"; import { EntityType, Format, QueryType, RouterType } from "@/services/types"; -import { IBlock } from "@/types/block.types"; +import { IBlock, IBlockStub } from "@/types/block.types"; import { BlockPorts } from "@/types/visual-editor.types"; import { BlockEditFormDialog } from "../BlockEditFormDialog"; @@ -80,6 +84,27 @@ const Diagrams = () => { const { searchPayload } = useSearch({ $eq: [{ category: selectedCategoryId }], }); + const selectedEntities = engine?.getModel()?.getSelectedEntities(); + const selectedLinks = (selectedEntities || []).filter( + (entity) => entity instanceof AdvancedLinkModel, + ); + const selectedBlocks = (selectedEntities || []).filter( + (entity) => entity instanceof NodeModel, + ); + const { toast } = useToast(); + const { mutate: duplicateBlock, isLoading: isDuplicatingBlock } = useCreate( + EntityType.BLOCK, + { + onError: () => { + toast.error(t("message.duplicate_block_error")); + }, + }, + ); + const shouldDisableDuplicateButton = + selectedLinks.length >= 1 || + selectedBlocks.length > 1 || + selectedBlocks.length === 0 || + isDuplicatingBlock; const { data: categories } = useFind( { entity: EntityType.CATEGORY }, { @@ -172,6 +197,23 @@ const Diagrams = () => { enabled: !!selectedCategoryId, }, ); + const handleDuplicateBlock = () => { + const selectedBlock = selectedBlocks[0] as any; + const block = getBlockFromCache(selectedBlock.options.id) as IBlockStub; + + if (!block) { + return; + } + + duplicateBlock({ + ...block, + name: `${block.name} (Copy)`, + position: { + x: block.position.x + 100, + y: block.position.y + 100, + }, + }); + }; useEffect(() => { // Case when categories are already cached @@ -670,6 +712,15 @@ const Diagrams = () => { > {t("button.move")} +