feat: move blocks between categories

This commit is contained in:
hexastack 2024-10-23 15:25:52 +01:00
parent 1792b125d3
commit 902596ce7f
4 changed files with 175 additions and 6 deletions

View File

@ -106,7 +106,8 @@
"no_label_found": "No label found",
"code_is_required": "Language code is required",
"text_is_required": "Text is required",
"invalid_file_type": "Invalid file type"
"invalid_file_type": "Invalid file type",
"select_category": "Select a flow"
},
"menu": {
"terms": "Terms of Use",
@ -505,6 +506,7 @@
"rename": "Rename",
"duplicate": "Duplicate",
"remove": "Remove",
"move": "Move",
"remove_permanently": "Remove",
"restore": "Restore",
"edit": "Edit",

View File

@ -106,7 +106,8 @@
"no_label_found": "Aucune étiquette trouvée",
"code_is_required": "Le code est requis",
"text_is_required": "Texte requis",
"invalid_file_type": "Type de fichier invalide"
"invalid_file_type": "Type de fichier invalide",
"select_category": "Sélectionner une catégorie"
},
"menu": {
"terms": "Conditions d'utilisation",
@ -506,6 +507,7 @@
"rename": "Renommer",
"duplicate": "Dupliquer",
"remove": "Supprimer",
"move": "Déplacer",
"remove_permanently": "Supprimer de façon permanente",
"restore": "Restaurer",
"edit": "Modifier",

View File

@ -0,0 +1,86 @@
/*
* 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 {
Button,
Dialog,
DialogActions,
DialogContent,
Grid,
MenuItem,
Select,
} from "@mui/material";
import { FC, useState } from "react";
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
import { DialogControl } from "@/hooks/useDialog";
import { useTranslate } from "@/hooks/useTranslate";
import { ICategory } from "@/types/category.types";
export interface MoveDialogProps extends DialogControl<string> {
categories: ICategory[];
callback?: (newCategoryId?: string) => Promise<void>;
openDialog: (data?: string) => void;
}
export const MoveDialog: FC<MoveDialogProps> = ({
open,
callback,
closeDialog,
categories,
}: MoveDialogProps) => {
const { t } = useTranslate();
const [selectedCategoryId, setSelectedCategoryId] = useState<string>("");
const handleMove = async () => {
if (selectedCategoryId && callback) {
await callback(selectedCategoryId);
closeDialog();
}
};
return (
<Dialog open={open} fullWidth onClose={closeDialog}>
<DialogTitle onClose={closeDialog}>
{t("message.select_category")}
</DialogTitle>
<DialogContent>
<Grid container direction="column" gap={2}>
<Grid item>
<Select
value={selectedCategoryId}
onChange={(e) => setSelectedCategoryId(e.target.value as string)}
fullWidth
displayEmpty
>
<MenuItem value="" disabled>
{t("label.category")}
</MenuItem>
{categories.map((category) => (
<MenuItem key={category.id} value={category.id}>
{category.label}
</MenuItem>
))}
</Select>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={handleMove}
disabled={!selectedCategoryId}
>
{t("button.move")}
</Button>
<Button variant="outlined" onClick={closeDialog}>
{t("button.cancel")}
</Button>
</DialogActions>
</Dialog>
);
};

View File

@ -11,6 +11,7 @@ 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 MoveIcon from "@mui/icons-material/Swipe";
import ZoomInIcon from "@mui/icons-material/ZoomIn";
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
import {
@ -38,6 +39,7 @@ import {
} from "react";
import { DeleteDialog } from "@/app-components/dialogs";
import { MoveDialog } from "@/app-components/dialogs/MoveDialog";
import { CategoryDialog } from "@/components/categories/CategoryDialog";
import { useDelete, useDeleteFromCache } from "@/hooks/crud/useDelete";
import { useFind } from "@/hooks/crud/useFind";
@ -67,6 +69,13 @@ const Diagrams = () => {
const [canvas, setCanvas] = useState<JSX.Element | undefined>();
const [selectedBlockId, setSelectedBlockId] = useState<string | undefined>();
const deleteDialogCtl = useDialog<string>(false);
const moveDialogCtl = useDialog<string>(false);
const { refetch: refetchBlocks } = useFind(
{ entity: EntityType.BLOCK, format: Format.FULL },
{
hasCount: false,
},
);
const addCategoryDialogCtl = useDialog<ICategory>(false);
const {
buildDiagram,
@ -174,10 +183,13 @@ const Diagrams = () => {
}, []);
useEffect(() => {
const filteredBlocks = blocks.filter(
(block) => block.category === selectedCategoryId,
);
const { canvas, model, engine } = buildDiagram({
zoom: currentCategory?.zoom || 100,
offset: currentCategory?.offset || [0, 0],
data: blocks,
data: filteredBlocks,
setter: setSelectedBlockId,
updateFn: updateBlock,
onRemoveNode: (ids, next) => {
@ -291,11 +303,15 @@ const Diagrams = () => {
zoomUpdated: debouncedZoomEvent,
offsetUpdated: debouncedOffsetEvent,
});
refetchBlocks();
}, [
selectedCategoryId,
JSON.stringify(
blocks.map((b) => {
return { ...b, position: undefined, updatedAt: undefined };
}),
blocks
.filter((b) => b.category === selectedCategoryId)
.map((b) => {
return { ...b, position: undefined, updatedAt: undefined };
}),
),
]);
@ -316,6 +332,14 @@ const Diagrams = () => {
deleteDialogCtl.openDialog(ids);
}
};
const handleMoveButton = () => {
const selectedEntities = engine?.getModel().getSelectedEntities();
const ids = selectedEntities?.map((model) => model.getID()).join(",");
if (ids && selectedEntities) {
moveDialogCtl.openDialog(ids);
}
};
const onDelete = async () => {
const id = deleteDialogCtl?.data;
@ -429,6 +453,45 @@ const Diagrams = () => {
deleteDialogCtl.closeDialog();
}
};
const onMove = async (newCategoryId?: string) => {
if (!newCategoryId) {
return;
}
const id = moveDialogCtl?.data;
if (id) {
const ids = id.includes(",") ? id.split(",") : [id];
for (const blockId of ids) {
const block = getBlockFromCache(blockId);
await updateBlock(
{
id: blockId,
params: {
category: newCategoryId,
},
},
{
onSuccess() {
updateCachedBlock({
id: blockId,
payload: {
...block,
category: newCategoryId,
},
strategy: "overwrite",
});
},
},
);
}
setSelectedCategoryId(newCategoryId);
setSelectedBlockId(undefined);
moveDialogCtl.closeDialog();
}
};
return (
<div
@ -462,6 +525,13 @@ const Diagrams = () => {
<CategoryDialog {...getDisplayDialogs(addCategoryDialogCtl)} />
<BlockDialog {...getDisplayDialogs(editDialogCtl)} />
<DeleteDialog {...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={{
@ -580,6 +650,15 @@ const Diagrams = () => {
>
{t("button.remove")}
</Button>
<Button
size="small"
variant="contained"
startIcon={<MoveIcon />}
onClick={handleMoveButton}
disabled={!selectedBlockId || selectedBlockId.length !== 24}
>
{t("button.move")}
</Button>
</Grid>
<Grid container item justifyContent="right" xs alignSelf="center">
<ButtonGroup