From fb8f1ba98bbb115f17dc18f3459bc15e44bc8062 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 10 Feb 2025 19:10:18 +0100 Subject: [PATCH 1/2] feat: duplicate block api --- api/src/chat/controllers/block.controller.ts | 5 ++++ api/src/chat/services/block.service.ts | 27 +++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/api/src/chat/controllers/block.controller.ts b/api/src/chat/controllers/block.controller.ts index 65535702a..308409ab2 100644 --- a/api/src/chat/controllers/block.controller.ts +++ b/api/src/chat/controllers/block.controller.ts @@ -341,4 +341,9 @@ export class BlockController extends BaseController< this.logger.log(`Successfully deleted blocks with IDs: ${ids}`); return deleteResult; } + + @Post('/duplicate/:blockId') + async duplicateBlock(@Param('blockId') blockId: string) { + return this.blockService.duplicateBlock(blockId); + } } diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index bab883878..3b340ceb9 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -6,7 +6,7 @@ * 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 { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { AttachmentService } from '@/attachment/services/attachment.service'; import EventWrapper from '@/channel/lib/EventWrapper'; @@ -55,6 +55,31 @@ export class BlockService extends BaseService< super(repository); } + async duplicateBlock(blockId: string) { + const block = await this.repository.findOne(blockId); + if (!block) { + throw new NotFoundException(`Unable to find ${blockId} to duplicate`); + } + const { + createdAt: _createdAt, + updatedAt: _updatedAt, + id: _id, + previousBlocks: _previousBlocks, + attachedBlock: _attachedBlock, + nextBlocks: _nextBlocks, + ...blockData + } = block; + + return await this.repository.create({ + ...blockData, + position: { + x: block.position.x, + y: block.position.y + 20, + }, + name: `${block.name} (Copy)`, + }); + } + /** * Find a block whose patterns matches the received event * From eda7afb624d37c111a211b4f9d74471555bf8f64 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Tue, 11 Feb 2025 09:30:23 +0100 Subject: [PATCH 2/2] feat: duplicate block --- frontend/public/locales/en/translation.json | 3 +- frontend/public/locales/fr/translation.json | 3 +- .../components/visual-editor/v2/Diagrams.tsx | 32 ++++++++++++- .../v2/hooks/useDuplicateBlock.tsx | 47 +++++++++++++++++++ frontend/src/services/api.class.ts | 10 +++- 5 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/visual-editor/v2/hooks/useDuplicateBlock.tsx diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index f7810d3f9..322c63df9 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 e257e4fd8..fa45a2782 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 924cb9ba7..082e719fe 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -1,11 +1,12 @@ /* - * 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 DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; @@ -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"; @@ -56,6 +58,7 @@ import { ZOOM_LEVEL } from "../constants"; import { useVisualEditor } from "../hooks/useVisualEditor"; import { AdvancedLinkModel } from "./AdvancedLink/AdvancedLinkModel"; +import { useDuplicateBlock } from "./hooks/useDuplicateBlock"; const Diagrams = () => { const { t } = useTranslate(); @@ -80,6 +83,13 @@ 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 { data: categories } = useFind( { entity: EntityType.CATEGORY }, { @@ -113,6 +123,7 @@ const Diagrams = () => { setSelectedBlockId(undefined); }, }); + const { mutate: duplicateBlock, isLoading } = useDuplicateBlock(); const { mutate: updateBlock } = useUpdate(EntityType.BLOCK, { invalidate: false, }); @@ -513,6 +524,11 @@ const Diagrams = () => { ); } }; + const handleDuplicateBlock = () => { + const ids = selectedBlocks[0].getID(); + + duplicateBlock({ blockId: ids }); + }; return (
{ > {t("button.move")} +