>;
+
+export interface OpenDialog {
+ /**
+ * Open a dialog without payload.
+ * @param Component The dialog component to open.
+ * @param options Additional options for the dialog.
+ */
+ (
+ Component: DialogComponent
,
+ payload?: P,
+ options?: OpenDialogOptions,
+ ): Promise;
+ /**
+ * Open a dialog and pass a payload.
+ * @param Component The dialog component to open.
+ * @param payload The payload to pass to the dialog.
+ * @param options Additional options for the dialog.
+ */
+ (
+ Component: DialogComponent
,
+ payload: P,
+ options?: OpenDialogOptions,
+ ): Promise;
+}
+
+export interface CloseDialog {
+ /**
+ * Close a dialog and return a result.
+ * @param dialog The dialog to close. The promise returned by `open`.
+ * @param result The result to return from the dialog.
+ * @returns A promise that resolves when the dialog is fully closed.
+ */
+ (dialog: Promise, result: R): Promise;
+}
+
+export interface DialogHook {
+ // alert: OpenAlertDialog;
+ // confirm: OpenConfirmDialog;
+ // prompt: OpenPromptDialog;
+ open: OpenDialog;
+ close: CloseDialog;
+}
+
+export const DialogsContext = React.createContext<{
+ open: OpenDialog;
+ close: CloseDialog;
+} | null>(null);
+
+interface DialogStackEntry {
+ key: string;
+ open: boolean;
+ promise: Promise;
+ Component: DialogComponent;
+ payload: P;
+ onClose: (result: R) => Promise;
+ resolve: (result: R) => void;
+}
+
+export interface DialogProviderProps {
+ children?: React.ReactNode;
+ unmountAfter?: number;
+}
+
+/**
+ * Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to
+ * access the dialogs API. The dialogs are rendered in the order they are requested.
+ *
+ * Demos:
+ *
+ * - [useDialogs](https://mui.com/toolpad/core/react-use-dialogs/)
+ *
+ * API:
+ *
+ * - [DialogsProvider API](https://mui.com/toolpad/core/api/dialogs-provider)
+ */
+function DialogsProvider(props: DialogProviderProps) {
+ const { children, unmountAfter = 1000 } = props;
+ const [stack, setStack] = React.useState[]>([]);
+ const keyPrefix = React.useId();
+ const nextId = React.useRef(0);
+ const requestDialog = React.useCallback(
+ function open(
+ Component: DialogComponent
,
+ payload: P,
+ options: OpenDialogOptions = {},
+ ) {
+ const { onClose = async () => {} } = options;
+ let resolve: ((result: R) => void) | undefined;
+ const promise = new Promise((resolveImpl) => {
+ resolve = resolveImpl;
+ });
+
+ if (!resolve) {
+ throw new Error("resolve not set");
+ }
+
+ const key = `${keyPrefix}-${nextId.current}`;
+
+ nextId.current += 1;
+
+ const newEntry: DialogStackEntry = {
+ key,
+ open: true,
+ promise,
+ Component,
+ payload,
+ onClose,
+ resolve,
+ };
+
+ setStack((prevStack) => [...prevStack, newEntry]);
+
+ return promise;
+ },
+ [keyPrefix],
+ );
+ const closeDialogUi = React.useCallback(
+ function closeDialogUi(dialog: Promise) {
+ setStack((prevStack) =>
+ prevStack.map((entry) =>
+ entry.promise === dialog ? { ...entry, open: false } : entry,
+ ),
+ );
+ setTimeout(() => {
+ // wait for closing animation
+ setStack((prevStack) =>
+ prevStack.filter((entry) => entry.promise !== dialog),
+ );
+ }, unmountAfter);
+ },
+ [unmountAfter],
+ );
+ const closeDialog = React.useCallback(
+ async function closeDialog(dialog: Promise, result: R) {
+ const entryToClose = stack.find((entry) => entry.promise === dialog);
+
+ if (!entryToClose) {
+ throw new Error("dialog not found");
+ }
+
+ await entryToClose.onClose(result);
+ entryToClose.resolve(result);
+ closeDialogUi(dialog);
+
+ return dialog;
+ },
+ [stack, closeDialogUi],
+ );
+ const contextValue = React.useMemo(
+ () => ({ open: requestDialog, close: closeDialog }),
+ [requestDialog, closeDialog],
+ );
+
+ return (
+
+ {children}
+ {stack.map(({ key, open, Component, payload, promise }) => (
+ {
+ await closeDialog(promise, result);
+ }}
+ />
+ ))}
+
+ );
+}
+
+export { DialogsProvider };
diff --git a/frontend/src/hooks/useDialogs.ts b/frontend/src/hooks/useDialogs.ts
new file mode 100644
index 00000000..7a481e48
--- /dev/null
+++ b/frontend/src/hooks/useDialogs.ts
@@ -0,0 +1,126 @@
+/*
+ * 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 { useContext, useMemo } from "react";
+
+import {
+ CloseDialog,
+ DialogsContext,
+ OpenDialog,
+} from "@/contexts/dialogs.context";
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+/*
+ * 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).
+ */
+
+export interface DialogHook {
+ // alert: OpenAlertDialog;
+ // confirm: OpenConfirmDialog;
+ // prompt: OpenPromptDialog;
+ open: OpenDialog;
+ close: CloseDialog;
+}
+
+export function useDialogs(): DialogHook {
+ const { open, close } = useContext(DialogsContext);
+ // const alert = React.useCallback(
+ // async (msg, { onClose, ...options } = {}) =>
+ // open(AlertDialog, { ...options, msg }, { onClose }),
+ // [open],
+ // );
+ // const confirm = React.useCallback(
+ // async (msg, { onClose, ...options } = {}) =>
+ // open(ConfirmDialog, { ...options, msg }, { onClose }),
+ // [open],
+ // );
+ // const prompt = React.useCallback(
+ // async (msg, { onClose, ...options } = {}) =>
+ // open(PromptDialog, { ...options, msg }, { onClose }),
+ // [open],
+ // );
+
+ return useMemo(
+ () => ({
+ // alert,
+ // confirm,
+ // prompt,
+ open,
+ close,
+ }),
+ // [alert, close, confirm, open, prompt],
+ [close, open],
+ );
+}
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index a0b2067d..d41af17f 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -20,6 +20,7 @@ import { SnackbarCloseButton } from "@/app-components/displays/Toast/CloseButton
import { ApiClientProvider } from "@/contexts/apiClient.context";
import { AuthProvider } from "@/contexts/auth.context";
import { ConfigProvider } from "@/contexts/config.context";
+import { DialogsProvider } from "@/contexts/dialogs.context";
import { PermissionProvider } from "@/contexts/permission.context";
import { SettingsProvider } from "@/contexts/setting.context";
import { ToastProvider } from "@/hooks/useToast";
@@ -72,31 +73,33 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => {
- (
-
- )}
- >
-
-
-
-
-
-
-
-
- {getLayout()}
-
-
-
-
-
-
-
-
-
+
+ (
+
+ )}
+ >
+
+
+
+
+
+
+
+
+ {getLayout()}
+
+
+
+
+
+
+
+
+
+