diff --git a/frontend/src/components/visual-editor/BlockDialog.tsx b/frontend/src/components/visual-editor/BlockDialog.tsx deleted file mode 100644 index 27bb00e8..00000000 --- a/frontend/src/components/visual-editor/BlockDialog.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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 ChatBubbleOutlineOutlinedIcon from "@mui/icons-material/ChatBubbleOutlineOutlined"; -import SettingsApplicationsIcon from "@mui/icons-material/SettingsApplications"; -import { - Dialog, - DialogActions, - DialogContent, - FormControlLabel, - Grid, - Switch, - Tab, - Tabs, -} from "@mui/material"; -import { FC, useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; - -import DialogButtons from "@/app-components/buttons/DialogButtons"; -import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; -import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; -import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem"; -import { Input } from "@/app-components/inputs/Input"; -import TriggerIcon from "@/app-components/svg/TriggerIcon"; -import { TabPanel } from "@/app-components/tabs/TabPanel"; -import { useUpdate } from "@/hooks/crud/useUpdate"; -import { DialogControlProps } from "@/hooks/useDialog"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { EntityType } from "@/services/types"; -import { OutgoingMessageFormat } from "@/types/message.types"; - -import { IBlock, IBlockAttributes } from "../../types/block.types"; - -import BlockFormProvider from "./form/BlockFormProvider"; -import { MessageForm } from "./form/MessageForm"; -import { OptionsForm } from "./form/OptionsForm"; -import { TriggersForm } from "./form/TriggersForm"; - -export type BlockDialogProps = DialogControlProps; -type TSelectedTab = "triggers" | "options" | "messages"; - -const BlockDialog: FC = ({ - open, - data: block, - closeDialog, - ...rest -}) => { - const { t } = useTranslate(); - const [selectedTab, setSelectedTab] = useState("triggers"); - const handleChange = ( - _event: React.SyntheticEvent, - newValue: TSelectedTab, - ) => { - setSelectedTab(newValue); - }; - const { toast } = useToast(); - const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const DEFAULT_VALUES = { - name: block?.name || "", - patterns: block?.patterns || [], - trigger_labels: block?.trigger_labels || [], - trigger_channels: block?.trigger_channels || [], - options: block?.options || { - typing: 0, - content: { - display: OutgoingMessageFormat.list, - top_element_style: "compact", - limit: 2, - }, - assignTo: block?.options?.assignTo, - fallback: block?.options?.fallback || { - active: true, - message: [], - max_attempts: 1, - }, - }, - assign_labels: block?.assign_labels || [], - message: block?.message || [""], - capture_vars: block?.capture_vars || [], - } as IBlockAttributes; - const methods = useForm({ - defaultValues: DEFAULT_VALUES, - }); - const { - reset, - register, - formState: { errors }, - handleSubmit, - control, - } = methods; - const validationRules = { - name: { - required: t("message.name_is_required"), - }, - }; - const onSubmitForm = async (params: IBlockAttributes) => { - if (block) { - updateBlock({ id: block.id, params }); - } - }; - - useEffect(() => { - if (open) { - reset(); - setSelectedTab("triggers"); - } - }, [open, reset]); - - useEffect(() => { - if (block && open) { - reset(DEFAULT_VALUES); - } else { - reset(); - } - }, [block, reset]); - - return ( - - - {t("title.edit_block")} - - - - - - ( - } - /> - )} - /> - - - - } - iconPosition="start" - /> - } - iconPosition="start" - /> - } - iconPosition="start" - /> - - - - - - - - - - - - - - - - - - - - - ); -}; - -BlockDialog.displayName = "BlockDialog"; - -export default BlockDialog; diff --git a/frontend/src/components/visual-editor/BlockEditForm.tsx b/frontend/src/components/visual-editor/BlockEditForm.tsx new file mode 100644 index 00000000..9a162ee3 --- /dev/null +++ b/frontend/src/components/visual-editor/BlockEditForm.tsx @@ -0,0 +1,178 @@ +/* + * 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 ChatBubbleOutlineOutlinedIcon from "@mui/icons-material/ChatBubbleOutlineOutlined"; +import SettingsApplicationsIcon from "@mui/icons-material/SettingsApplications"; +import { FormControlLabel, Grid, Switch, Tab, Tabs } from "@mui/material"; +import { FC, Fragment, useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; + +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; +import { Input } from "@/app-components/inputs/Input"; +import TriggerIcon from "@/app-components/svg/TriggerIcon"; +import { TabPanel } from "@/app-components/tabs/TabPanel"; +import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useToast } from "@/hooks/useToast"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType } from "@/services/types"; +import { IBlock, IBlockAttributes } from "@/types/block.types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; +import { OutgoingMessageFormat } from "@/types/message.types"; + +import BlockFormProvider from "./form/BlockFormProvider"; +import { MessageForm } from "./form/MessageForm"; +import { OptionsForm } from "./form/OptionsForm"; +import { TriggersForm } from "./form/TriggersForm"; + +type TSelectedTab = "triggers" | "options" | "messages"; + +export const BlockEditForm: FC> = ({ + data: block, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { t } = useTranslate(); + const [selectedTab, setSelectedTab] = useState("triggers"); + const handleChange = ( + _event: React.SyntheticEvent, + newValue: TSelectedTab, + ) => { + setSelectedTab(newValue); + }; + const { toast } = useToast(); + const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, { + onError: () => { + rest.onError?.(); + toast.error(t("message.internal_server_error")); + }, + onSuccess: () => { + rest.onSuccess?.(); + toast.success(t("message.success_save")); + }, + }); + const DEFAULT_VALUES = { + name: block?.name || "", + patterns: block?.patterns || [], + trigger_labels: block?.trigger_labels || [], + trigger_channels: block?.trigger_channels || [], + options: block?.options || { + typing: 0, + content: { + display: OutgoingMessageFormat.list, + top_element_style: "compact", + limit: 2, + }, + assignTo: block?.options?.assignTo, + fallback: block?.options?.fallback || { + active: true, + message: [], + max_attempts: 1, + }, + }, + assign_labels: block?.assign_labels || [], + message: block?.message || [""], + capture_vars: block?.capture_vars || [], + } as IBlockAttributes; + const methods = useForm({ + defaultValues: DEFAULT_VALUES, + }); + const { + reset, + register, + formState: { errors }, + handleSubmit, + control, + } = methods; + const validationRules = { + name: { + required: t("message.name_is_required"), + }, + }; + const onSubmitForm = async (params: IBlockAttributes) => { + if (block) { + updateBlock({ id: block.id, params }); + } + }; + + useEffect(() => { + if (block) { + reset(DEFAULT_VALUES); + } else { + reset(); + } + }, [block, reset]); + + return ( + + + + + + + ( + } + /> + )} + /> + + + + } + iconPosition="start" + /> + } + iconPosition="start" + /> + } + iconPosition="start" + /> + + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/src/components/visual-editor/BlockEditFormDialog.tsx b/frontend/src/components/visual-editor/BlockEditFormDialog.tsx new file mode 100644 index 00000000..ce3f1013 --- /dev/null +++ b/frontend/src/components/visual-editor/BlockEditFormDialog.tsx @@ -0,0 +1,25 @@ +/* + * 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 { GenericFormDialog } from "@/app-components/dialogs"; +import { IBlock } from "@/types/block.types"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; + +import { BlockEditForm } from "./BlockEditForm"; + +export const BlockEditFormDialog = < + T extends IBlock | undefined = IBlock | undefined, +>( + props: ComponentFormDialogProps, +) => ( + + Form={BlockEditForm} + editText="title.edit_block" + {...props} + /> +); diff --git a/frontend/src/components/visual-editor/BlockMoveForm.tsx b/frontend/src/components/visual-editor/BlockMoveForm.tsx new file mode 100644 index 00000000..53443576 --- /dev/null +++ b/frontend/src/components/visual-editor/BlockMoveForm.tsx @@ -0,0 +1,65 @@ +/* + * 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 { MenuItem, Select } from "@mui/material"; +import { FC, Fragment, useState } from "react"; + +import { ContentContainer } from "@/app-components/dialogs/"; +import { ICategory } from "@/types/category.types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; + +export type BlockMoveFormData = { + row?: never; + ids: string[]; + onMove: (ids: string[], targetCategoryId: string) => void; + category: string; + categories: ICategory[]; +}; + +export const BlockMoveForm: FC> = ({ + data, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const [selectedCategoryId, setSelectedCategoryId] = useState( + data?.category || "", + ); + const handleMove = () => { + if (selectedCategoryId) { + data?.onMove(data.ids, selectedCategoryId); + rest.onSuccess?.(); + } + }; + + return ( + + + + + + ); +}; diff --git a/frontend/src/components/visual-editor/BlockMoveFormDialog.tsx b/frontend/src/components/visual-editor/BlockMoveFormDialog.tsx new file mode 100644 index 00000000..48c33986 --- /dev/null +++ b/frontend/src/components/visual-editor/BlockMoveFormDialog.tsx @@ -0,0 +1,28 @@ +/* + * 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 { MoveUp } from "@mui/icons-material"; + +import { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; + +import { BlockMoveForm, BlockMoveFormData } from "./BlockMoveForm"; + +export const BlockMoveFormDialog = < + T extends BlockMoveFormData = BlockMoveFormData, +>( + props: ComponentFormDialogProps, +) => ( + + Form={BlockMoveForm} + rowKey="row" + addText="message.select_category" + confirmButtonProps={{ value: "button.move", startIcon: }} + {...props} + /> +); diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 7e6237b0..6cfa7d80 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -30,18 +30,12 @@ import { DiagramModelGenerics, } from "@projectstorm/react-diagrams"; import { useRouter } from "next/router"; -import { - SyntheticEvent, - useCallback, - useEffect, - useRef, - useState, -} from "react"; +import { SyntheticEvent, useCallback, useEffect, useState } from "react"; import { useQueryClient } from "react-query"; -import { DeleteDialog } from "@/app-components/dialogs"; -import { MoveDialog } from "@/app-components/dialogs/MoveDialog"; +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 { useDeleteFromCache } from "@/hooks/crud/useDelete"; import { useDeleteMany } from "@/hooks/crud/useDeleteMany"; @@ -50,7 +44,6 @@ 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 { useDialogs } from "@/hooks/useDialogs"; import { useSearch } from "@/hooks/useSearch"; import { useTranslate } from "@/hooks/useTranslate"; @@ -58,7 +51,7 @@ import { EntityType, Format, QueryType, RouterType } from "@/services/types"; import { IBlock } from "@/types/block.types"; import { BlockPorts } from "@/types/visual-editor.types"; -import BlockDialog from "../BlockDialog"; +import { BlockEditFormDialog } from "../BlockEditFormDialog"; import { ZOOM_LEVEL } from "../constants"; import { useVisualEditor } from "../hooks/useVisualEditor"; @@ -75,9 +68,7 @@ const Diagrams = () => { const [canvas, setCanvas] = useState(); const [selectedBlockId, setSelectedBlockId] = useState(); const dialogs = useDialogs(); - const deleteDialogCtl = useDialog(false); - const moveDialogCtl = useDialog(false); - const { mutateAsync: updateBlocks } = useUpdateMany(EntityType.BLOCK); + const { mutate: updateBlocks } = useUpdateMany(EntityType.BLOCK); const { buildDiagram, setViewerZoom, @@ -86,7 +77,6 @@ const Diagrams = () => { selectedCategoryId, createNode, } = useVisualEditor(); - const editDialogCtl = useDialog(false); const { searchPayload } = useSearch({ $eq: [{ category: selectedCategoryId }], }); @@ -97,7 +87,9 @@ const Diagrams = () => { initialSortState: [{ field: "createdAt", sort: "asc" }], }, { - onSuccess([{ id, zoom, offset }]) { + onSuccess(categories) { + const { id, zoom, offset } = categories[0] || {}; + if (flowId) { setSelectedCategoryId?.(flowId); } else if (id) { @@ -113,12 +105,11 @@ const Diagrams = () => { const currentCategory = categories.find( ({ id }) => id === selectedCategoryId, ); - const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, { + const { mutate: updateCategory } = useUpdate(EntityType.CATEGORY, { invalidate: false, }); - const { mutateAsync: deleteBlocks } = useDeleteMany(EntityType.BLOCK, { + const { mutate: deleteBlocks } = useDeleteMany(EntityType.BLOCK, { onSuccess: () => { - deleteDialogCtl.closeDialog(); setSelectedBlockId(undefined); }, }); @@ -159,9 +150,9 @@ const Diagrams = () => { const getBlockFromCache = useGetFromCache(EntityType.BLOCK); const updateCachedBlock = useUpdateCache(EntityType.BLOCK); const deleteCachedBlock = useDeleteFromCache(EntityType.BLOCK); - const handleChange = (_event: SyntheticEvent, newValue: number) => { + const onCategoryChange = (targetCategory: number) => { if (categories) { - const { id } = categories[newValue]; + const { id } = categories[targetCategory]; if (id) { setSelectedCategoryId?.(id); @@ -171,6 +162,9 @@ const Diagrams = () => { } } }; + const handleChange = (_event: SyntheticEvent, newValue: number) => { + onCategoryChange(newValue); + }; const { data: blocks } = useFind( { entity: EntityType.BLOCK, format: Format.FULL }, { hasCount: false, params: searchPayload }, @@ -178,7 +172,6 @@ const Diagrams = () => { enabled: !!selectedCategoryId, }, ); - const deleteCallbackRef = useRef<() => void | null>(() => {}); useEffect(() => { // Case when categories are already cached @@ -202,15 +195,14 @@ const Diagrams = () => { data: blocks, setter: setSelectedBlockId, updateFn: updateBlock, - onRemoveNode: (ids, next) => { - deleteDialogCtl.openDialog(ids); - deleteCallbackRef.current = next; - }, + onRemoveNode: openDeleteDialog, onDbClickNode: (event, id) => { if (id) { const block = getBlockFromCache(id); - editDialogCtl.openDialog(block); + dialogs.open(BlockEditFormDialog, block, { + maxWidth: "md", + }); } }, targetPortChanged: ({ @@ -322,24 +314,24 @@ const Diagrams = () => { ), ]); - const handleLinkDeletion = async (linkId: string) => { + const handleLinkDeletion = (linkId: string, model: DiagramModel) => { 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); + removeNextBlockLink(sourceId, targetId); } else if ( link?.sourcePort.options.label === BlockPorts.attachmentOutPort ) { - await removeAttachmentLink(sourceId, targetId); + removeAttachedLink(sourceId, targetId); } }; - const removeNextBlockLink = async (sourceId: string, targetId: string) => { + const removeNextBlockLink = (sourceId: string, targetId: string) => { const previousData = getBlockFromCache(sourceId); const nextBlocks = [...(previousData?.nextBlocks || [])]; - await updateBlock( + updateBlock( { id: sourceId, params: { @@ -361,8 +353,8 @@ const Diagrams = () => { }, ); }; - const removeAttachmentLink = async (sourceId: string, targetId: string) => { - await updateBlock( + const removeAttachedLink = (sourceId: string, targetId: string) => { + updateBlock( { id: sourceId, params: { attachedBlock: null }, @@ -377,8 +369,8 @@ const Diagrams = () => { }, ); }; - const handleBlocksDeletion = async (blockIds: string[]) => { - await deleteBlocks(blockIds, { + const handleBlocksDeletion = (blockIds: string[]) => { + deleteBlocks(blockIds, { onSuccess: () => { blockIds.forEach((blockId) => { const block = getBlockFromCache(blockId); @@ -428,74 +420,93 @@ const Diagrams = () => { } }); }; - const cleanupAfterDeletion = () => { - deleteCallbackRef.current?.(); - deleteCallbackRef.current = () => {}; - deleteDialogCtl.closeDialog(); - }; - const handleDeleteButton = () => { - const selectedEntities = engine?.getModel().getSelectedEntities(); - const ids = selectedEntities?.map((model) => model.getID()); + const getSelectedIds = () => { + const entities = engine?.getModel().getSelectedEntities(); + const ids = entities?.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); + return ids || []; + }; + const getGroupedIds = (ids: string[]) => { + return ids.reduce( + (acc, str) => ({ + ...acc, + ...(str.length === 36 + ? { linkIds: [...acc.linkIds, str] } + : { blockIds: [...acc.blockIds, str] }), + }), + { linkIds: [] as string[], blockIds: [] as string[] }, + ); + }; + const hasSelectedBlock = () => { + const ids = getSelectedIds(); + + return getGroupedIds(ids).blockIds.length > 0; + }; + const openDeleteDialog = async () => { + const ids = getSelectedIds(); + const model = engine?.getModel(); + + if (ids.length) { + const isConfirmed = await dialogs.confirm(ConfirmDialogBody, { + mode: "selection", + count: ids.length, + isSingleton: true, + }); + + if (isConfirmed && model) { + onDelete(ids, model); + } } }; const handleMoveButton = () => { - const selectedEntities = engine?.getModel().getSelectedEntities().reverse(); - const ids = selectedEntities?.map((model) => model.getID()); + const ids = getSelectedIds(); + const { blockIds } = getGroupedIds(ids); - if (ids && selectedEntities) { - moveDialogCtl.openDialog(ids); + if (ids.length) { + dialogs.open(BlockMoveFormDialog, { + ids: blockIds, + onMove, + category: selectedCategoryId, + categories, + }); } }; - const onDelete = async () => { - const ids = deleteDialogCtl?.data; - + const onDelete = (ids: string[], model: DiagramModel) => { if (!ids || ids?.length === 0) { return; } - const isLink = ids[0].length === 36; - if (isLink) { - await handleLinkDeletion(ids[0]); - } else { - await handleBlocksDeletion(ids); + const { linkIds, blockIds } = getGroupedIds(ids); + + if (linkIds.length && !blockIds.length) { + linkIds.forEach((linkId) => handleLinkDeletion(linkId, model)); + } else if (blockIds.length) { + handleBlocksDeletion(blockIds); } - - cleanupAfterDeletion(); }; - const onMove = async (newCategoryId?: string) => { - if (!newCategoryId) { - return; - } + const onMove = (ids: string[], targetCategoryId: string) => { + if (ids.length) { + updateBlocks( + { ids, payload: { category: targetCategoryId } }, + { + onSuccess() { + queryClient.invalidateQueries({ + predicate: ({ queryKey }) => { + const [qType, qEntity] = queryKey; - const ids = moveDialogCtl?.data; + return ( + qType === QueryType.collection && + isSameEntity(qEntity, EntityType.BLOCK) + ); + }, + }); - 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) - ); + onCategoryChange( + categories.findIndex(({ id }) => id === targetCategoryId), + ); + }, }, - }); - - setSelectedCategoryId(newCategoryId); - setSelectedBlockId(undefined); - moveDialogCtl.closeDialog(); + ); } }; @@ -528,15 +539,6 @@ const Diagrams = () => { }} > - - {...deleteDialogCtl} callback={onDelete} /> - { backgroundColor: "#F8F8F8", borderBottom: "none", minHeight: "30px", - "&.Mui-selected": { backgroundColor: "#EAF1F1", zIndex: 1, color: "#000", - backgroundSize: "20px 20px", backgroundAttachment: "fixed", backgroundPosition: "-1px -1px", @@ -617,15 +617,14 @@ const Diagrams = () => { ml: "5px", borderRadius: "0", minHeight: "30px", - border: "1px solid #DDDDDD", backgroundColor: "#F8F8F8", borderBottom: "none", width: "42px", minWidth: "42px", }} - onClick={async (e) => { - await dialogs.open(CategoryFormDialog, null); + onClick={(e) => { + dialogs.open(CategoryFormDialog, null); e.preventDefault(); }} > @@ -653,10 +652,12 @@ const Diagrams = () => { if (selectedBlockId) { const block = getBlockFromCache(selectedBlockId); - editDialogCtl.openDialog(block); + dialogs.open(BlockEditFormDialog, block, { + maxWidth: "md", + }); } }} - disabled={!selectedBlockId || selectedBlockId.length !== 24} + disabled={getSelectedIds().length > 1 || !hasSelectedBlock()} > {t("button.edit")} @@ -665,7 +666,7 @@ const Diagrams = () => { variant="contained" startIcon={} onClick={handleMoveButton} - disabled={!selectedBlockId || selectedBlockId.length !== 24} + disabled={!hasSelectedBlock()} > {t("button.move")} @@ -675,8 +676,8 @@ const Diagrams = () => { variant="contained" color="secondary" startIcon={} - onClick={handleDeleteButton} - disabled={!selectedBlockId} + onClick={() => openDeleteDialog()} + disabled={!getSelectedIds().length} > {t("button.remove")} @@ -698,7 +699,6 @@ const Diagrams = () => { "&.MuiButtonGroup-contained:hover": { boxShadow: "0 0 8px #0005", }, - "& .MuiButton-root": { backgroundColor: "background.paper", }, diff --git a/frontend/src/contexts/dialogs.context.tsx b/frontend/src/contexts/dialogs.context.tsx index 753af19f..17740828 100644 --- a/frontend/src/contexts/dialogs.context.tsx +++ b/frontend/src/contexts/dialogs.context.tsx @@ -47,6 +47,8 @@ export const DialogsContext = createContext< function DialogsProvider(props: DialogProviderProps) { const { children, unmountAfter = 1000 } = props; const [stack, setStack] = useState[]>([]); + let selectComponent: (typeof stack)[number]["Component"] | undefined = + undefined; const keyPrefix = useId(); const nextId = useRef(0); const requestDialog = useCallback( @@ -80,7 +82,10 @@ function DialogsProvider(props: DialogProviderProps) { msgProps: rest, }; - setStack((prevStack) => [...prevStack, newEntry]); + if (selectComponent !== Component || !rest.isSingleton) { + selectComponent = Component; + setStack((prevStack) => [...prevStack, newEntry]); + } return promise; }, @@ -99,6 +104,7 @@ function DialogsProvider(props: DialogProviderProps) { prevStack.filter((entry) => entry.promise !== dialog), ); }, unmountAfter); + selectComponent = undefined; }, [unmountAfter], ); diff --git a/frontend/src/hooks/useDialogs.ts b/frontend/src/hooks/useDialogs.ts index 4b6ef29c..c1b1ce74 100644 --- a/frontend/src/hooks/useDialogs.ts +++ b/frontend/src/hooks/useDialogs.ts @@ -32,7 +32,7 @@ export const useDialogs = (): DialogHook => { const { open, close } = context; const confirm = React.useCallback( async (msg, { onClose, ...options } = {}) => { - const { count, mode, ...rest } = options; + const { count, mode, isSingleton, ...rest } = options; return open( ConfirmDialog, @@ -44,6 +44,7 @@ export const useDialogs = (): DialogHook => { mode, count, onClose, + isSingleton, }, ); }, diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index d5b010b2..943c1c92 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -14,6 +14,7 @@ interface DialogExtraOptions { count?: number; maxWidth?: MuiDialogProps["maxWidth"]; hasButtons?: boolean; + isSingleton?: boolean; } // context export interface OpenDialogOptions extends DialogExtraOptions {