mirror of
https://github.com/hexastack/hexabot
synced 2024-11-24 04:53:41 +00:00
Merge pull request #267 from Hexastack/266-issue-incomplete-block-removal-during-bulk-delete-operation
fix: incomplete blocks removal during delete operation
This commit is contained in:
commit
135c4b928c
@ -323,4 +323,29 @@ export class BlockController extends BaseController<
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple blocks by their IDs.
|
||||||
|
* @param ids - IDs of blocks to be deleted.
|
||||||
|
* @returns A Promise that resolves to the deletion result.
|
||||||
|
*/
|
||||||
|
@CsrfCheck(true)
|
||||||
|
@Delete('')
|
||||||
|
@HttpCode(204)
|
||||||
|
async deleteMany(@Body('ids') ids: string[]): Promise<DeleteResult> {
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
throw new BadRequestException('No IDs provided for deletion.');
|
||||||
|
}
|
||||||
|
const deleteResult = await this.blockService.deleteMany({
|
||||||
|
_id: { $in: ids },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleteResult.deletedCount === 0) {
|
||||||
|
this.logger.warn(`Unable to delete blocks with provided IDs: ${ids}`);
|
||||||
|
throw new NotFoundException('Blocks with provided IDs not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Successfully deleted blocks with IDs: ${ids}`);
|
||||||
|
return deleteResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,25 +8,24 @@
|
|||||||
|
|
||||||
import ErrorIcon from "@mui/icons-material/Error";
|
import ErrorIcon from "@mui/icons-material/Error";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
Grid,
|
Grid,
|
||||||
Typography,
|
Typography,
|
||||||
DialogContent,
|
|
||||||
Button,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||||
import { DialogControl } from "@/hooks/useDialog";
|
import { DialogControl } from "@/hooks/useDialog";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
|
||||||
export type DeleteDialogProps = DialogControl<string>;
|
export type DeleteDialogProps<T = string> = DialogControl<T>;
|
||||||
export const DeleteDialog: FC<DeleteDialogProps> = ({
|
export const DeleteDialog = <T extends any = string>({
|
||||||
open,
|
open,
|
||||||
callback,
|
callback,
|
||||||
closeDialog: closeFunction,
|
closeDialog: closeFunction,
|
||||||
}: DeleteDialogProps) => {
|
}: DeleteDialogProps<T>) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -42,7 +42,8 @@ import { DeleteDialog } from "@/app-components/dialogs";
|
|||||||
import { MoveDialog } from "@/app-components/dialogs/MoveDialog";
|
import { MoveDialog } from "@/app-components/dialogs/MoveDialog";
|
||||||
import { CategoryDialog } from "@/components/categories/CategoryDialog";
|
import { CategoryDialog } from "@/components/categories/CategoryDialog";
|
||||||
import { isSameEntity } from "@/hooks/crud/helpers";
|
import { isSameEntity } from "@/hooks/crud/helpers";
|
||||||
import { useDelete, useDeleteFromCache } from "@/hooks/crud/useDelete";
|
import { useDeleteFromCache } from "@/hooks/crud/useDelete";
|
||||||
|
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
|
||||||
import { useFind } from "@/hooks/crud/useFind";
|
import { useFind } from "@/hooks/crud/useFind";
|
||||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||||
import { useUpdate, useUpdateCache } from "@/hooks/crud/useUpdate";
|
import { useUpdate, useUpdateCache } from "@/hooks/crud/useUpdate";
|
||||||
@ -70,7 +71,7 @@ const Diagrams = () => {
|
|||||||
const [engine, setEngine] = useState<DiagramEngine | undefined>();
|
const [engine, setEngine] = useState<DiagramEngine | undefined>();
|
||||||
const [canvas, setCanvas] = useState<JSX.Element | undefined>();
|
const [canvas, setCanvas] = useState<JSX.Element | undefined>();
|
||||||
const [selectedBlockId, setSelectedBlockId] = useState<string | undefined>();
|
const [selectedBlockId, setSelectedBlockId] = useState<string | undefined>();
|
||||||
const deleteDialogCtl = useDialog<string>(false);
|
const deleteDialogCtl = useDialog<string[]>(false);
|
||||||
const moveDialogCtl = useDialog<string[] | string>(false);
|
const moveDialogCtl = useDialog<string[] | string>(false);
|
||||||
const addCategoryDialogCtl = useDialog<ICategory>(false);
|
const addCategoryDialogCtl = useDialog<ICategory>(false);
|
||||||
const { mutateAsync: updateBlocks } = useUpdateMany(EntityType.BLOCK);
|
const { mutateAsync: updateBlocks } = useUpdateMany(EntityType.BLOCK);
|
||||||
@ -89,10 +90,7 @@ const Diagrams = () => {
|
|||||||
const { data: categories } = useFind(
|
const { data: categories } = useFind(
|
||||||
{ entity: EntityType.CATEGORY },
|
{ entity: EntityType.CATEGORY },
|
||||||
{
|
{
|
||||||
initialPaginationState: {
|
hasCount: false,
|
||||||
page: 0,
|
|
||||||
pageSize: 999, // @TODO: We need to display all categories
|
|
||||||
},
|
|
||||||
initialSortState: [{ field: "createdAt", sort: "asc" }],
|
initialSortState: [{ field: "createdAt", sort: "asc" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -113,12 +111,11 @@ const Diagrams = () => {
|
|||||||
const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, {
|
const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, {
|
||||||
invalidate: false,
|
invalidate: false,
|
||||||
});
|
});
|
||||||
const { mutateAsync: deleteBlock } = useDelete(EntityType.BLOCK, {
|
const { mutateAsync: deleteBlocks } = useDeleteMany(EntityType.BLOCK, {
|
||||||
onSuccess() {
|
onSuccess: () => {
|
||||||
deleteDialogCtl.closeDialog();
|
deleteDialogCtl.closeDialog();
|
||||||
setSelectedBlockId(undefined);
|
setSelectedBlockId(undefined);
|
||||||
},
|
},
|
||||||
invalidate: false,
|
|
||||||
});
|
});
|
||||||
const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, {
|
const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, {
|
||||||
invalidate: false,
|
invalidate: false,
|
||||||
@ -181,6 +178,7 @@ const Diagrams = () => {
|
|||||||
if (categories?.length > 0 && !selectedCategoryId) {
|
if (categories?.length > 0 && !selectedCategoryId) {
|
||||||
setSelectedCategoryId(categories[0].id);
|
setSelectedCategoryId(categories[0].id);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -191,7 +189,7 @@ const Diagrams = () => {
|
|||||||
setter: setSelectedBlockId,
|
setter: setSelectedBlockId,
|
||||||
updateFn: updateBlock,
|
updateFn: updateBlock,
|
||||||
onRemoveNode: (ids, next) => {
|
onRemoveNode: (ids, next) => {
|
||||||
deleteDialogCtl.openDialog(ids.join(","));
|
deleteDialogCtl.openDialog(ids);
|
||||||
deleteCallbackRef.current = next;
|
deleteCallbackRef.current = next;
|
||||||
},
|
},
|
||||||
onDbClickNode: (event, id) => {
|
onDbClickNode: (event, id) => {
|
||||||
@ -310,44 +308,20 @@ const Diagrams = () => {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDeleteButton = () => {
|
const handleLinkDeletion = async (linkId: string) => {
|
||||||
const selectedEntities = engine?.getModel().getSelectedEntities();
|
const link = model?.getLink(linkId) as any;
|
||||||
const ids = selectedEntities?.map((model) => model.getID()).join(",");
|
|
||||||
|
|
||||||
if (ids && selectedEntities) {
|
|
||||||
deleteCallbackRef.current = () => {
|
|
||||||
if (selectedEntities.length > 0) {
|
|
||||||
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 id = deleteDialogCtl?.data;
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
// Check if it's a link id
|
|
||||||
if (id.length === 36) {
|
|
||||||
// Remove link + update nextBlocks + TODO update port state
|
|
||||||
const link = model?.getLink(id) as any;
|
|
||||||
const sourceId = link?.sourcePort.parent.options.id;
|
const sourceId = link?.sourcePort.parent.options.id;
|
||||||
const targetId = link?.targetPort.parent.options.id;
|
const targetId = link?.targetPort.parent.options.id;
|
||||||
|
|
||||||
if (link?.sourcePort.options.label === BlockPorts.nextBlocksOutPort) {
|
if (link?.sourcePort.options.label === BlockPorts.nextBlocksOutPort) {
|
||||||
// Next/previous Link Delete
|
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 previousData = getBlockFromCache(sourceId);
|
||||||
const nextBlocks = [...(previousData?.nextBlocks || [])];
|
const nextBlocks = [...(previousData?.nextBlocks || [])];
|
||||||
|
|
||||||
@ -365,89 +339,125 @@ const Diagrams = () => {
|
|||||||
preprocess: ({ previousBlocks = [], ...rest }) => ({
|
preprocess: ({ previousBlocks = [], ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
previousBlocks: previousBlocks.filter(
|
previousBlocks: previousBlocks.filter(
|
||||||
(previousBlock) => previousBlock !== sourceId,
|
(block) => block !== sourceId,
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (
|
};
|
||||||
link?.sourcePort.options.label === BlockPorts.attachmentOutPort
|
const removeAttachmentLink = async (sourceId: string, targetId: string) => {
|
||||||
) {
|
|
||||||
// Attached / AttachedTo Link Delete
|
|
||||||
await updateBlock(
|
await updateBlock(
|
||||||
{
|
{
|
||||||
id: sourceId,
|
id: sourceId,
|
||||||
params: {
|
params: { attachedBlock: null },
|
||||||
attachedBlock: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
updateCachedBlock({
|
updateCachedBlock({
|
||||||
id: targetId,
|
id: targetId,
|
||||||
preprocess: (oldData) => ({
|
preprocess: (oldData) => ({ ...oldData, attachedToBlock: null }),
|
||||||
...oldData,
|
|
||||||
attachedToBlock: null,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
} else {
|
const handleBlocksDeletion = async (blockIds: string[]) => {
|
||||||
// Block Delete Case
|
await deleteBlocks(blockIds, {
|
||||||
const ids = id.includes(",") ? id.split(",") : [id];
|
onSuccess: () => {
|
||||||
const deletePromises = ids.map((id) => {
|
blockIds.forEach((blockId) => {
|
||||||
const block = getBlockFromCache(id);
|
const block = getBlockFromCache(blockId);
|
||||||
|
|
||||||
return deleteBlock(id, {
|
if (block) {
|
||||||
onSuccess() {
|
updateLinkedBlocks(block, blockIds);
|
||||||
// Update all linked blocks to remove any reference to the deleted block
|
deleteCachedBlock(blockId);
|
||||||
[
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const getLinkedBlockIds = (block: IBlock): string[] => [
|
||||||
...(block?.nextBlocks || []),
|
...(block?.nextBlocks || []),
|
||||||
...(block?.previousBlocks || []),
|
...(block?.previousBlocks || []),
|
||||||
...(block?.attachedBlock ? [block.attachedBlock] : []),
|
...(block?.attachedBlock ? [block.attachedBlock] : []),
|
||||||
...(block?.attachedToBlock ? [block.attachedToBlock] : []),
|
...(block?.attachedToBlock ? [block.attachedToBlock] : []),
|
||||||
]
|
];
|
||||||
.map((bid) => getBlockFromCache(bid))
|
const updateLinkedBlocks = (block: IBlock, deletedIds: string[]) => {
|
||||||
.filter((b) => !!b)
|
const linkedBlockIds = getLinkedBlockIds(block);
|
||||||
.forEach((b) => {
|
|
||||||
|
linkedBlockIds.forEach((linkedBlockId) => {
|
||||||
|
const linkedBlock = getBlockFromCache(linkedBlockId);
|
||||||
|
|
||||||
|
if (linkedBlock) {
|
||||||
updateCachedBlock({
|
updateCachedBlock({
|
||||||
id: b.id,
|
id: linkedBlock.id,
|
||||||
payload: {
|
payload: {
|
||||||
...b,
|
...linkedBlock,
|
||||||
nextBlocks: b.nextBlocks?.filter(
|
nextBlocks: linkedBlock.nextBlocks?.filter(
|
||||||
(nextBlockId) => nextBlockId !== id,
|
(nextBlockId) => !deletedIds.includes(nextBlockId),
|
||||||
),
|
),
|
||||||
previousBlocks: b.previousBlocks?.filter(
|
previousBlocks: linkedBlock.previousBlocks?.filter(
|
||||||
(previousBlockId) => previousBlockId !== id,
|
(previousBlockId) => !deletedIds.includes(previousBlockId),
|
||||||
),
|
),
|
||||||
attachedBlock:
|
attachedBlock: deletedIds.includes(linkedBlock.attachedBlock || "")
|
||||||
b.attachedBlock === id ? undefined : b.attachedBlock,
|
|
||||||
attachedToBlock:
|
|
||||||
b.attachedToBlock === id
|
|
||||||
? undefined
|
? undefined
|
||||||
: b.attachedToBlock,
|
: linkedBlock.attachedBlock,
|
||||||
|
attachedToBlock: deletedIds.includes(
|
||||||
|
linkedBlock.attachedToBlock || "",
|
||||||
|
)
|
||||||
|
? undefined
|
||||||
|
: linkedBlock.attachedToBlock,
|
||||||
},
|
},
|
||||||
strategy: "overwrite",
|
strategy: "overwrite",
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
deleteCachedBlock(id);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(deletePromises);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const cleanupAfterDeletion = () => {
|
||||||
deleteCallbackRef.current?.();
|
deleteCallbackRef.current?.();
|
||||||
deleteCallbackRef.current = () => {};
|
deleteCallbackRef.current = () => {};
|
||||||
deleteDialogCtl.closeDialog();
|
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) => {
|
const onMove = async (newCategoryId?: string) => {
|
||||||
if (!newCategoryId) {
|
if (!newCategoryId) {
|
||||||
return;
|
return;
|
||||||
@ -506,7 +516,7 @@ const Diagrams = () => {
|
|||||||
<Box sx={{ width: "100%" }}>
|
<Box sx={{ width: "100%" }}>
|
||||||
<CategoryDialog {...getDisplayDialogs(addCategoryDialogCtl)} />
|
<CategoryDialog {...getDisplayDialogs(addCategoryDialogCtl)} />
|
||||||
<BlockDialog {...getDisplayDialogs(editDialogCtl)} />
|
<BlockDialog {...getDisplayDialogs(editDialogCtl)} />
|
||||||
<DeleteDialog {...deleteDialogCtl} callback={onDelete} />
|
<DeleteDialog<string[]> {...deleteDialogCtl} callback={onDelete} />
|
||||||
<MoveDialog
|
<MoveDialog
|
||||||
open={moveDialogCtl.open}
|
open={moveDialogCtl.open}
|
||||||
openDialog={moveDialogCtl.openDialog}
|
openDialog={moveDialogCtl.openDialog}
|
||||||
|
Loading…
Reference in New Issue
Block a user