mirror of
https://github.com/hexastack/hexabot
synced 2025-05-08 14:54:45 +00:00
802 lines
24 KiB
TypeScript
802 lines
24 KiB
TypeScript
/*
|
|
* Copyright © 2024 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";
|
|
import FitScreenIcon from "@mui/icons-material/FitScreen";
|
|
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
|
import ZoomInIcon from "@mui/icons-material/ZoomIn";
|
|
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
|
|
import {
|
|
Box,
|
|
Button,
|
|
ButtonGroup,
|
|
Grid,
|
|
Tab,
|
|
Tabs,
|
|
Tooltip,
|
|
tabsClasses,
|
|
} from "@mui/material";
|
|
import {
|
|
DefaultPortModel,
|
|
DiagramEngine,
|
|
DiagramModel,
|
|
DiagramModelGenerics,
|
|
} from "@projectstorm/react-diagrams";
|
|
import { useRouter } from "next/router";
|
|
import {
|
|
SyntheticEvent,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { useQueryClient } from "react-query";
|
|
|
|
import { DeleteDialog } from "@/app-components/dialogs";
|
|
import { MoveDialog } from "@/app-components/dialogs/MoveDialog";
|
|
import { CategoryDialog } from "@/components/categories/CategoryDialog";
|
|
import { isSameEntity } from "@/hooks/crud/helpers";
|
|
import { useDeleteFromCache } from "@/hooks/crud/useDelete";
|
|
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
|
|
import { useFind } from "@/hooks/crud/useFind";
|
|
import { useGetFromCache } from "@/hooks/crud/useGet";
|
|
import { useUpdate, useUpdateCache } from "@/hooks/crud/useUpdate";
|
|
import { useUpdateMany } from "@/hooks/crud/useUpdateMany";
|
|
import useDebouncedUpdate from "@/hooks/useDebouncedUpdate";
|
|
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
|
import { useSearch } from "@/hooks/useSearch";
|
|
import { useTranslate } from "@/hooks/useTranslate";
|
|
import { EntityType, Format, QueryType, RouterType } from "@/services/types";
|
|
import { IBlock } from "@/types/block.types";
|
|
import { ICategory } from "@/types/category.types";
|
|
import { BlockPorts } from "@/types/visual-editor.types";
|
|
|
|
import BlockDialog from "../BlockDialog";
|
|
import { ZOOM_LEVEL } from "../constants";
|
|
import { useVisualEditor } from "../hooks/useVisualEditor";
|
|
|
|
import { AdvancedLinkModel } from "./AdvancedLink/AdvancedLinkModel";
|
|
|
|
const Diagrams = () => {
|
|
const { t } = useTranslate();
|
|
const router = useRouter();
|
|
const flowId = router.query.id?.toString();
|
|
const [model, setModel] = useState<
|
|
DiagramModel<DiagramModelGenerics> | undefined
|
|
>();
|
|
const [engine, setEngine] = useState<DiagramEngine | undefined>();
|
|
const [canvas, setCanvas] = useState<JSX.Element | undefined>();
|
|
const [selectedBlockId, setSelectedBlockId] = useState<string | undefined>();
|
|
const deleteDialogCtl = useDialog<string[]>(false);
|
|
const moveDialogCtl = useDialog<string[] | string>(false);
|
|
const addCategoryDialogCtl = useDialog<ICategory>(false);
|
|
const { mutateAsync: updateBlocks } = useUpdateMany(EntityType.BLOCK);
|
|
const {
|
|
buildDiagram,
|
|
setViewerZoom,
|
|
setViewerOffset,
|
|
setSelectedCategoryId,
|
|
selectedCategoryId,
|
|
createNode,
|
|
} = useVisualEditor();
|
|
const editDialogCtl = useDialog<IBlock>(false);
|
|
const { searchPayload } = useSearch<IBlock>({
|
|
$eq: [{ category: selectedCategoryId }],
|
|
});
|
|
const { data: categories } = useFind(
|
|
{ entity: EntityType.CATEGORY },
|
|
{
|
|
hasCount: false,
|
|
initialSortState: [{ field: "createdAt", sort: "asc" }],
|
|
},
|
|
{
|
|
onSuccess([{ id, zoom, offset }]) {
|
|
if (flowId) {
|
|
setSelectedCategoryId?.(flowId);
|
|
} else if (id) {
|
|
setSelectedCategoryId?.(id);
|
|
if (engine?.getModel()) {
|
|
setViewerOffset(offset || [0, 0]);
|
|
setViewerZoom(zoom || 100);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
);
|
|
const currentCategory = categories.find(
|
|
({ id }) => id === selectedCategoryId,
|
|
);
|
|
const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, {
|
|
invalidate: false,
|
|
});
|
|
const { mutateAsync: deleteBlocks } = useDeleteMany(EntityType.BLOCK, {
|
|
onSuccess: () => {
|
|
deleteDialogCtl.closeDialog();
|
|
setSelectedBlockId(undefined);
|
|
},
|
|
});
|
|
const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, {
|
|
invalidate: false,
|
|
});
|
|
const debouncedUpdateCategory = useDebouncedUpdate(updateCategory, 300);
|
|
const debouncedZoomEvent = useCallback(
|
|
(event: any) => {
|
|
if (selectedCategoryId) {
|
|
engine?.repaintCanvas();
|
|
debouncedUpdateCategory({
|
|
id: selectedCategoryId,
|
|
params: {
|
|
zoom: event.zoom,
|
|
},
|
|
});
|
|
}
|
|
event.stopPropagation();
|
|
},
|
|
[selectedCategoryId, engine, debouncedUpdateCategory],
|
|
);
|
|
const debouncedOffsetEvent = useCallback(
|
|
(event: any) => {
|
|
if (selectedCategoryId) {
|
|
debouncedUpdateCategory({
|
|
id: selectedCategoryId,
|
|
params: {
|
|
offset: [event.offsetX, event.offsetY],
|
|
},
|
|
});
|
|
}
|
|
event.stopPropagation();
|
|
},
|
|
[selectedCategoryId, debouncedUpdateCategory],
|
|
);
|
|
const queryClient = useQueryClient();
|
|
const getBlockFromCache = useGetFromCache(EntityType.BLOCK);
|
|
const updateCachedBlock = useUpdateCache(EntityType.BLOCK);
|
|
const deleteCachedBlock = useDeleteFromCache(EntityType.BLOCK);
|
|
const handleChange = (_event: SyntheticEvent, newValue: number) => {
|
|
if (categories) {
|
|
const { id } = categories[newValue];
|
|
|
|
if (id) {
|
|
setSelectedCategoryId?.(id);
|
|
setSelectedBlockId(undefined); // Reset selected block when switching categories, resetting edit & remove buttons
|
|
|
|
router.push(`/${RouterType.VISUAL_EDITOR}/flows/${id}`);
|
|
}
|
|
}
|
|
};
|
|
const { data: blocks } = useFind(
|
|
{ entity: EntityType.BLOCK, format: Format.FULL },
|
|
{ hasCount: false, params: searchPayload },
|
|
{
|
|
enabled: !!selectedCategoryId,
|
|
},
|
|
);
|
|
const deleteCallbackRef = useRef<() => void | null>(() => {});
|
|
|
|
useEffect(() => {
|
|
// Case when categories are already cached
|
|
if (categories?.length > 0 && !selectedCategoryId) {
|
|
setSelectedCategoryId(categories[0].id);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (flowId) setSelectedCategoryId(flowId);
|
|
else if (categories?.length) setSelectedCategoryId(categories[0].id);
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [flowId]);
|
|
|
|
useEffect(() => {
|
|
const { canvas, model, engine } = buildDiagram({
|
|
zoom: currentCategory?.zoom || 100,
|
|
offset: currentCategory?.offset || [0, 0],
|
|
data: blocks,
|
|
setter: setSelectedBlockId,
|
|
updateFn: updateBlock,
|
|
onRemoveNode: (ids, next) => {
|
|
deleteDialogCtl.openDialog(ids);
|
|
deleteCallbackRef.current = next;
|
|
},
|
|
onDbClickNode: (event, id) => {
|
|
if (id) {
|
|
const block = getBlockFromCache(id);
|
|
|
|
editDialogCtl.openDialog(block);
|
|
}
|
|
},
|
|
targetPortChanged: ({
|
|
entity,
|
|
port,
|
|
}: {
|
|
entity: AdvancedLinkModel;
|
|
port: DefaultPortModel;
|
|
}) => {
|
|
const link = model.getLink(entity.getOptions().id as string);
|
|
|
|
if (!link) return;
|
|
|
|
if (
|
|
!port.getOptions().in ||
|
|
[BlockPorts.nextBlocksOutPort, BlockPorts.attachmentOutPort].includes(
|
|
// @ts-expect-error protected attr
|
|
entity.targetPort.getOptions().label,
|
|
) ||
|
|
(link.getSourcePort().getType() === "attached" &&
|
|
link.getSourcePort().getParent().getOptions().id ===
|
|
link.getTargetPort().getParent().getOptions().id)
|
|
) {
|
|
model.removeLink(link);
|
|
|
|
return;
|
|
}
|
|
|
|
link.setLocked(true);
|
|
link.registerListener({
|
|
selectionChanged(event: any) {
|
|
const { entity, isSelected } = event;
|
|
|
|
setSelectedBlockId(isSelected === true && entity.options.id);
|
|
},
|
|
});
|
|
|
|
Object.entries(port.links).map(([, val]) => {
|
|
// @ts-expect-error protected attr
|
|
if (val.targetPort?.options?.locked) {
|
|
model.removeLink(val);
|
|
}
|
|
});
|
|
|
|
const sourceId = entity.getSourcePort().getParent().getOptions()
|
|
.id as string;
|
|
const targetId = entity.getTargetPort().getParent().getOptions()
|
|
.id as string;
|
|
const previousData = getBlockFromCache(sourceId!);
|
|
|
|
if (
|
|
// @ts-expect-error undefined attr
|
|
entity.getSourcePort().getOptions()?.label ===
|
|
BlockPorts.nextBlocksOutPort
|
|
) {
|
|
const nextBlocks = [
|
|
...(previousData?.nextBlocks || []),
|
|
...(targetId ? [targetId] : []),
|
|
];
|
|
|
|
updateBlock(
|
|
{
|
|
id: sourceId,
|
|
params: {
|
|
nextBlocks,
|
|
},
|
|
},
|
|
{
|
|
onSuccess(data) {
|
|
if (data.id)
|
|
updateCachedBlock({
|
|
id: targetId,
|
|
payload: {
|
|
previousBlocks: [data.id as any],
|
|
},
|
|
});
|
|
},
|
|
},
|
|
);
|
|
} else if (
|
|
// @ts-expect-error undefined attr
|
|
entity.getSourcePort().getOptions().label ===
|
|
BlockPorts.attachmentOutPort
|
|
) {
|
|
updateBlock({
|
|
id: sourceId,
|
|
params: {
|
|
attachedBlock: targetId,
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
setModel(model);
|
|
setEngine(engine);
|
|
setCanvas(canvas);
|
|
|
|
model.registerListener({
|
|
zoomUpdated: debouncedZoomEvent,
|
|
offsetUpdated: debouncedOffsetEvent,
|
|
});
|
|
}, [
|
|
selectedCategoryId,
|
|
JSON.stringify(
|
|
blocks.map((b) => {
|
|
return { ...b, position: undefined, updatedAt: undefined };
|
|
}),
|
|
),
|
|
]);
|
|
|
|
const handleLinkDeletion = async (linkId: string) => {
|
|
const link = model?.getLink(linkId) as any;
|
|
const sourceId = link?.sourcePort.parent.options.id;
|
|
const targetId = link?.targetPort.parent.options.id;
|
|
|
|
if (link?.sourcePort.options.label === BlockPorts.nextBlocksOutPort) {
|
|
await removeNextBlockLink(sourceId, targetId);
|
|
} else if (
|
|
link?.sourcePort.options.label === BlockPorts.attachmentOutPort
|
|
) {
|
|
await removeAttachmentLink(sourceId, targetId);
|
|
}
|
|
};
|
|
const removeNextBlockLink = async (sourceId: string, targetId: string) => {
|
|
const previousData = getBlockFromCache(sourceId);
|
|
const nextBlocks = [...(previousData?.nextBlocks || [])];
|
|
|
|
await updateBlock(
|
|
{
|
|
id: sourceId,
|
|
params: {
|
|
nextBlocks: nextBlocks.filter((block) => block !== targetId),
|
|
},
|
|
},
|
|
{
|
|
onSuccess() {
|
|
updateCachedBlock({
|
|
id: targetId,
|
|
preprocess: ({ previousBlocks = [], ...rest }) => ({
|
|
...rest,
|
|
previousBlocks: previousBlocks.filter(
|
|
(block) => block !== sourceId,
|
|
),
|
|
}),
|
|
});
|
|
},
|
|
},
|
|
);
|
|
};
|
|
const removeAttachmentLink = async (sourceId: string, targetId: string) => {
|
|
await updateBlock(
|
|
{
|
|
id: sourceId,
|
|
params: { attachedBlock: null },
|
|
},
|
|
{
|
|
onSuccess() {
|
|
updateCachedBlock({
|
|
id: targetId,
|
|
preprocess: (oldData) => ({ ...oldData, attachedToBlock: null }),
|
|
});
|
|
},
|
|
},
|
|
);
|
|
};
|
|
const handleBlocksDeletion = async (blockIds: string[]) => {
|
|
await deleteBlocks(blockIds, {
|
|
onSuccess: () => {
|
|
blockIds.forEach((blockId) => {
|
|
const block = getBlockFromCache(blockId);
|
|
|
|
if (block) {
|
|
updateLinkedBlocks(block, blockIds);
|
|
deleteCachedBlock(blockId);
|
|
}
|
|
});
|
|
},
|
|
});
|
|
};
|
|
const getLinkedBlockIds = (block: IBlock): string[] => [
|
|
...(block?.nextBlocks || []),
|
|
...(block?.previousBlocks || []),
|
|
...(block?.attachedBlock ? [block.attachedBlock] : []),
|
|
...(block?.attachedToBlock ? [block.attachedToBlock] : []),
|
|
];
|
|
const updateLinkedBlocks = (block: IBlock, deletedIds: string[]) => {
|
|
const linkedBlockIds = getLinkedBlockIds(block);
|
|
|
|
linkedBlockIds.forEach((linkedBlockId) => {
|
|
const linkedBlock = getBlockFromCache(linkedBlockId);
|
|
|
|
if (linkedBlock) {
|
|
updateCachedBlock({
|
|
id: linkedBlock.id,
|
|
payload: {
|
|
...linkedBlock,
|
|
nextBlocks: linkedBlock.nextBlocks?.filter(
|
|
(nextBlockId) => !deletedIds.includes(nextBlockId),
|
|
),
|
|
previousBlocks: linkedBlock.previousBlocks?.filter(
|
|
(previousBlockId) => !deletedIds.includes(previousBlockId),
|
|
),
|
|
attachedBlock: deletedIds.includes(linkedBlock.attachedBlock || "")
|
|
? undefined
|
|
: linkedBlock.attachedBlock,
|
|
attachedToBlock: deletedIds.includes(
|
|
linkedBlock.attachedToBlock || "",
|
|
)
|
|
? undefined
|
|
: linkedBlock.attachedToBlock,
|
|
},
|
|
strategy: "overwrite",
|
|
});
|
|
}
|
|
});
|
|
};
|
|
const cleanupAfterDeletion = () => {
|
|
deleteCallbackRef.current?.();
|
|
deleteCallbackRef.current = () => {};
|
|
deleteDialogCtl.closeDialog();
|
|
};
|
|
const handleDeleteButton = () => {
|
|
const selectedEntities = engine?.getModel().getSelectedEntities();
|
|
const ids = selectedEntities?.map((model) => model.getID());
|
|
|
|
if (ids && selectedEntities && ids.length > 0) {
|
|
deleteCallbackRef.current = () => {
|
|
selectedEntities.forEach((model) => {
|
|
model.setLocked(false);
|
|
model.remove();
|
|
});
|
|
engine?.repaintCanvas();
|
|
};
|
|
deleteDialogCtl.openDialog(ids);
|
|
}
|
|
};
|
|
const handleMoveButton = () => {
|
|
const selectedEntities = engine?.getModel().getSelectedEntities().reverse();
|
|
const ids = selectedEntities?.map((model) => model.getID());
|
|
|
|
if (ids && selectedEntities) {
|
|
moveDialogCtl.openDialog(ids);
|
|
}
|
|
};
|
|
const onDelete = async () => {
|
|
const ids = deleteDialogCtl?.data;
|
|
|
|
if (!ids || ids?.length === 0) {
|
|
return;
|
|
}
|
|
const isLink = ids[0].length === 36;
|
|
|
|
if (isLink) {
|
|
await handleLinkDeletion(ids[0]);
|
|
} else {
|
|
await handleBlocksDeletion(ids);
|
|
}
|
|
|
|
cleanupAfterDeletion();
|
|
};
|
|
const onMove = async (newCategoryId?: string) => {
|
|
if (!newCategoryId) {
|
|
return;
|
|
}
|
|
|
|
const ids = moveDialogCtl?.data;
|
|
|
|
if (ids?.length && Array.isArray(ids)) {
|
|
await updateBlocks({ ids, payload: { category: newCategoryId } });
|
|
|
|
queryClient.invalidateQueries({
|
|
predicate: ({ queryKey }) => {
|
|
const [qType, qEntity] = queryKey;
|
|
|
|
return (
|
|
qType === QueryType.collection &&
|
|
isSameEntity(qEntity, EntityType.BLOCK)
|
|
);
|
|
},
|
|
});
|
|
|
|
setSelectedCategoryId(newCategoryId);
|
|
setSelectedBlockId(undefined);
|
|
moveDialogCtl.closeDialog();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="visual-editor"
|
|
id="visual-editor"
|
|
onDrop={(event) => {
|
|
const data = JSON.parse(
|
|
event.dataTransfer.getData("storm-diagram-node"),
|
|
);
|
|
|
|
if (!data) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn("Unable to handle the drop event");
|
|
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
...data,
|
|
category: selectedCategoryId || "",
|
|
position: engine?.getRelativeMousePoint(event),
|
|
};
|
|
|
|
createNode(payload);
|
|
}}
|
|
onDragOver={(event) => {
|
|
event.preventDefault();
|
|
}}
|
|
>
|
|
<Box sx={{ width: "100%" }}>
|
|
<CategoryDialog {...getDisplayDialogs(addCategoryDialogCtl)} />
|
|
<BlockDialog {...getDisplayDialogs(editDialogCtl)} />
|
|
<DeleteDialog<string[]> {...deleteDialogCtl} callback={onDelete} />
|
|
<MoveDialog
|
|
open={moveDialogCtl.open}
|
|
openDialog={moveDialogCtl.openDialog}
|
|
callback={onMove}
|
|
closeDialog={moveDialogCtl.closeDialog}
|
|
categories={categories}
|
|
/>
|
|
<Grid sx={{ bgcolor: "#fff", padding: "0" }}>
|
|
<Grid
|
|
sx={{
|
|
display: "flex",
|
|
position: "relative",
|
|
flexDirection: "row",
|
|
borderBottom: "1.5px solid #DDDDDD",
|
|
}}
|
|
>
|
|
<Tabs
|
|
value={
|
|
currentCategory
|
|
? categories?.findIndex(({ id }) => id === selectedCategoryId)
|
|
: 0
|
|
}
|
|
onChange={handleChange}
|
|
sx={{
|
|
backgroundColor: "#fff",
|
|
[`& .${tabsClasses.indicator}`]: {
|
|
display: "none",
|
|
},
|
|
"& .MuiTabs-scrollButtons": {
|
|
opacity: 0.8,
|
|
backgroundColor: "#1ca089",
|
|
borderTop: "1px solid #137261",
|
|
marginTop: "7px",
|
|
color: "#FFF",
|
|
overflow: "visible",
|
|
boxShadow:
|
|
"-20px 0px 20px -20px rgba(0, 0, 0, 0.5), 0px 2px 9px 0px rgba(0, 0, 0, 0.5)",
|
|
zIndex: 10,
|
|
"&:hover": {
|
|
opacity: 1,
|
|
},
|
|
},
|
|
}}
|
|
variant="scrollable"
|
|
allowScrollButtonsMobile
|
|
>
|
|
{categories?.map(({ id, label }) => (
|
|
<Tab
|
|
sx={{
|
|
mt: "7px",
|
|
ml: "5px",
|
|
border: "1px solid #DDDDDD",
|
|
backgroundColor: "#F8F8F8",
|
|
borderBottom: "none",
|
|
minHeight: "30px",
|
|
|
|
"&.Mui-selected": {
|
|
backgroundColor: "#EAF1F1",
|
|
zIndex: 1,
|
|
color: "#000",
|
|
|
|
backgroundSize: "20px 20px",
|
|
backgroundAttachment: "fixed",
|
|
backgroundPosition: "-1px -1px",
|
|
},
|
|
}}
|
|
key={id}
|
|
label={
|
|
<Grid
|
|
sx={{
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
display: "flex",
|
|
}}
|
|
>
|
|
{label}
|
|
</Grid>
|
|
}
|
|
/>
|
|
))}
|
|
</Tabs>
|
|
<Button
|
|
sx={{
|
|
mt: "7px",
|
|
ml: "5px",
|
|
borderRadius: "0",
|
|
minHeight: "30px",
|
|
|
|
border: "1px solid #DDDDDD",
|
|
backgroundColor: "#F8F8F8",
|
|
borderBottom: "none",
|
|
width: "42px",
|
|
minWidth: "42px",
|
|
}}
|
|
onClick={(e) => {
|
|
addCategoryDialogCtl.openDialog();
|
|
e.preventDefault();
|
|
}}
|
|
>
|
|
<Add />
|
|
</Button>
|
|
</Grid>
|
|
<Grid container>
|
|
<Grid
|
|
sx={{
|
|
left: 240,
|
|
top: 140,
|
|
zIndex: 6,
|
|
position: "absolute",
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
gap: "8px",
|
|
}}
|
|
>
|
|
<Button
|
|
sx={{}}
|
|
size="small"
|
|
variant="contained"
|
|
startIcon={<EditIcon />}
|
|
onClick={() => {
|
|
if (selectedBlockId) {
|
|
const block = getBlockFromCache(selectedBlockId);
|
|
|
|
editDialogCtl.openDialog(block);
|
|
}
|
|
}}
|
|
disabled={!selectedBlockId || selectedBlockId.length !== 24}
|
|
>
|
|
{t("button.edit")}
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
variant="contained"
|
|
startIcon={<MoveUp />}
|
|
onClick={handleMoveButton}
|
|
disabled={!selectedBlockId || selectedBlockId.length !== 24}
|
|
>
|
|
{t("button.move")}
|
|
</Button>
|
|
<Button
|
|
sx={{}}
|
|
size="small"
|
|
variant="contained"
|
|
color="secondary"
|
|
startIcon={<DeleteIcon />}
|
|
onClick={handleDeleteButton}
|
|
disabled={!selectedBlockId}
|
|
>
|
|
{t("button.remove")}
|
|
</Button>
|
|
</Grid>
|
|
<Grid container item justifyContent="right" xs alignSelf="center">
|
|
<ButtonGroup
|
|
orientation="vertical"
|
|
color="inherit"
|
|
variant="contained"
|
|
sx={{
|
|
left: 240,
|
|
bottom: 20,
|
|
zIndex: 6,
|
|
position: "absolute",
|
|
transition: "all 0.3s",
|
|
boxShadow: "0 0 8px #0003",
|
|
borderRadius: "20px",
|
|
backgroundColor: "#fffc",
|
|
"&.MuiButtonGroup-contained:hover": {
|
|
boxShadow: "0 0 8px #0005",
|
|
},
|
|
|
|
"& .MuiButton-root": {
|
|
backgroundColor: "background.paper",
|
|
},
|
|
}}
|
|
>
|
|
<Tooltip
|
|
title={t("visual_editor.zoom.reset")}
|
|
arrow
|
|
placement="left"
|
|
>
|
|
<span>
|
|
<Button
|
|
sx={{ p: "8px 2px 6px 2px" }}
|
|
disabled={
|
|
!!currentCategory?.zoom && currentCategory?.zoom === 100
|
|
}
|
|
onClick={() => {
|
|
setViewerZoom(100);
|
|
}}
|
|
>
|
|
<RestartAltIcon />
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip
|
|
title={t("visual_editor.zoom.in")}
|
|
arrow
|
|
placement="left"
|
|
>
|
|
<span>
|
|
<Button
|
|
sx={{ p: "6px 2px" }}
|
|
disabled={
|
|
!!currentCategory?.zoom &&
|
|
currentCategory.zoom >= ZOOM_LEVEL.maximum
|
|
}
|
|
onClick={() => {
|
|
if (model) {
|
|
const currentZoom = model.getZoomLevel();
|
|
|
|
setViewerZoom(currentZoom + ZOOM_LEVEL.step);
|
|
}
|
|
}}
|
|
>
|
|
<ZoomInIcon />
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip
|
|
title={t("visual_editor.zoom.out")}
|
|
arrow
|
|
placement="left"
|
|
>
|
|
<span>
|
|
<Button
|
|
sx={{ p: "6px 2px" }}
|
|
disabled={
|
|
!!currentCategory?.zoom &&
|
|
currentCategory?.zoom <= ZOOM_LEVEL.minimum
|
|
}
|
|
onClick={() => {
|
|
if (model) {
|
|
const currentZoom = model.getZoomLevel();
|
|
|
|
setViewerZoom(currentZoom - ZOOM_LEVEL.step);
|
|
}
|
|
}}
|
|
>
|
|
<ZoomOutIcon />
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip
|
|
title={t("visual_editor.zoom.fitScreen")}
|
|
arrow
|
|
placement="left"
|
|
>
|
|
<span>
|
|
<Button
|
|
sx={{ p: "6px 2px 8px 2px" }}
|
|
onClick={() => {
|
|
engine?.zoomToFitSelectedNodes({ margin: 0 });
|
|
}}
|
|
>
|
|
<FitScreenIcon />
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
</ButtonGroup>
|
|
</Grid>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
{canvas}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Diagrams;
|