mirror of
https://github.com/hexastack/hexabot
synced 2025-04-27 01:39:59 +00:00
refactor: dialog system (partial)
This commit is contained in:
parent
1288eb87cf
commit
407581a27e
@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import {
|
import {
|
||||||
|
IconButton,
|
||||||
|
DialogTitle as MuiDialogTitle,
|
||||||
Typography,
|
Typography,
|
||||||
styled,
|
styled,
|
||||||
DialogTitle as MuiDialogTitle,
|
|
||||||
IconButton,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
const StyledDialogTitle = styled(Typography)(() => ({
|
const StyledDialogTitle = styled(Typography)(() => ({
|
||||||
@ -24,12 +24,20 @@ export const DialogTitle = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onClose?: () => void;
|
onClose?:
|
||||||
|
| ((event: {}, reason: "backdropClick" | "escapeKeyDown") => void)
|
||||||
|
| undefined;
|
||||||
}) => (
|
}) => (
|
||||||
<MuiDialogTitle>
|
<MuiDialogTitle>
|
||||||
<StyledDialogTitle>{children}</StyledDialogTitle>
|
<StyledDialogTitle>{children}</StyledDialogTitle>
|
||||||
<IconButton size="small" aria-label="close" onClick={onClose}>
|
{onClose && (
|
||||||
<CloseIcon />
|
<IconButton
|
||||||
</IconButton>
|
size="small"
|
||||||
|
aria-label="close"
|
||||||
|
onClick={(e) => onClose(e, "backdropClick")}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</MuiDialogTitle>
|
</MuiDialogTitle>
|
||||||
);
|
);
|
||||||
|
40
frontend/src/app-components/dialogs/FormDialog.tsx
Normal file
40
frontend/src/app-components/dialogs/FormDialog.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogProps,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||||
|
|
||||||
|
export interface FormDialogProps extends DialogProps {
|
||||||
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormDialog: FC<FormDialogProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} fullWidth onClose={onClose} {...rest}>
|
||||||
|
<DialogTitle onClose={onClose}>{title}</DialogTitle>
|
||||||
|
<DialogContent>{children}</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{/* <DialogButtons closeDialog={closeDialog} /> */}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
90
frontend/src/components/categories/CategoryForm.tsx
Normal file
90
frontend/src/components/categories/CategoryForm.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* 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 { FC, useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||||
|
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||||
|
import { Input } from "@/app-components/inputs/Input";
|
||||||
|
import { useCreate } from "@/hooks/crud/useCreate";
|
||||||
|
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||||
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
import { EntityType } from "@/services/types";
|
||||||
|
import { ICategory, ICategoryAttributes } from "@/types/category.types";
|
||||||
|
|
||||||
|
export type CategoryFormProps = {
|
||||||
|
data: ICategory | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CategoryForm: FC<CategoryFormProps> = ({ data }) => {
|
||||||
|
const { t } = useTranslate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { mutateAsync: createCategory } = useCreate(EntityType.CATEGORY, {
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("message.success_save"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, {
|
||||||
|
onError: () => {
|
||||||
|
toast.error(t("message.internal_server_error"));
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("message.success_save"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm<ICategoryAttributes>({
|
||||||
|
defaultValues: { label: data?.label || "" },
|
||||||
|
});
|
||||||
|
const validationRules = {
|
||||||
|
label: {
|
||||||
|
required: t("message.label_is_required"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const onSubmitForm = async (params: ICategoryAttributes) => {
|
||||||
|
if (data) {
|
||||||
|
updateCategory({ id: data.id, params });
|
||||||
|
} else {
|
||||||
|
createCategory(params);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset({
|
||||||
|
label: data.label,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||||
|
<ContentContainer>
|
||||||
|
<ContentItem>
|
||||||
|
<Input
|
||||||
|
label={t("placeholder.label")}
|
||||||
|
{...register("label", validationRules.label)}
|
||||||
|
autoFocus
|
||||||
|
helperText={errors.label ? errors.label.message : null}
|
||||||
|
/>
|
||||||
|
</ContentItem>
|
||||||
|
</ContentContainer>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
37
frontend/src/components/categories/CategoryFormDialog.tsx
Normal file
37
frontend/src/components/categories/CategoryFormDialog.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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 { FC } from "react";
|
||||||
|
|
||||||
|
import { FormDialog } from "@/app-components/dialogs/FormDialog";
|
||||||
|
import { DialogProps } from "@/contexts/dialogs.context";
|
||||||
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
import { ICategory } from "@/types/category.types";
|
||||||
|
|
||||||
|
import { CategoryForm } from "./CategoryForm";
|
||||||
|
|
||||||
|
export type CategoryFormProps = DialogProps<ICategory | null, boolean>;
|
||||||
|
|
||||||
|
export const CategoryFormDialog: FC<CategoryFormProps> = ({
|
||||||
|
onClose,
|
||||||
|
payload,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormDialog
|
||||||
|
title={payload ? t("title.edit_category") : t("title.new_category")}
|
||||||
|
{...rest}
|
||||||
|
// @TODO: fix typing
|
||||||
|
onClose={async () => await onClose(false)}
|
||||||
|
>
|
||||||
|
<CategoryForm data={payload} />
|
||||||
|
</FormDialog>
|
||||||
|
);
|
||||||
|
};
|
@ -13,7 +13,6 @@ import { Button, Grid, Paper } from "@mui/material";
|
|||||||
import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
|
import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog";
|
|
||||||
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||||
import {
|
import {
|
||||||
ActionColumnLabel,
|
ActionColumnLabel,
|
||||||
@ -24,7 +23,8 @@ import { DataGrid } from "@/app-components/tables/DataGrid";
|
|||||||
import { useDelete } from "@/hooks/crud/useDelete";
|
import { useDelete } from "@/hooks/crud/useDelete";
|
||||||
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
|
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
|
||||||
import { useFind } from "@/hooks/crud/useFind";
|
import { useFind } from "@/hooks/crud/useFind";
|
||||||
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
import { useDialog } from "@/hooks/useDialog";
|
||||||
|
import { useDialogs } from "@/hooks/useDialogs";
|
||||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||||
import { useSearch } from "@/hooks/useSearch";
|
import { useSearch } from "@/hooks/useSearch";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
@ -36,13 +36,11 @@ import { getDateTimeFormatter } from "@/utils/date";
|
|||||||
|
|
||||||
import { ICategory } from "../../types/category.types";
|
import { ICategory } from "../../types/category.types";
|
||||||
|
|
||||||
import { CategoryDialog } from "./CategoryDialog";
|
import { CategoryFormDialog } from "./CategoryFormDialog";
|
||||||
|
|
||||||
export const Categories = () => {
|
export const Categories = () => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const addDialogCtl = useDialog<ICategory>(false);
|
|
||||||
const editDialogCtl = useDialog<ICategory>(false);
|
|
||||||
const deleteDialogCtl = useDialog<string>(false);
|
const deleteDialogCtl = useDialog<string>(false);
|
||||||
const hasPermission = useHasPermission();
|
const hasPermission = useHasPermission();
|
||||||
const { onSearch, searchPayload } = useSearch<ICategory>({
|
const { onSearch, searchPayload } = useSearch<ICategory>({
|
||||||
@ -80,7 +78,7 @@ export const Categories = () => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: ActionColumnLabel.Edit,
|
label: ActionColumnLabel.Edit,
|
||||||
action: (row) => editDialogCtl.openDialog(row),
|
action: (row) => dialogs.open(CategoryFormDialog, row),
|
||||||
requires: [PermissionAction.UPDATE],
|
requires: [PermissionAction.UPDATE],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -128,10 +126,11 @@ export const Categories = () => {
|
|||||||
const handleSelectionChange = (selection: GridRowSelectionModel) => {
|
const handleSelectionChange = (selection: GridRowSelectionModel) => {
|
||||||
setSelectedCategories(selection as string[]);
|
setSelectedCategories(selection as string[]);
|
||||||
};
|
};
|
||||||
|
const dialogs = useDialogs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container gap={3} flexDirection="column">
|
<Grid container gap={3} flexDirection="column">
|
||||||
<CategoryDialog {...getDisplayDialogs(addDialogCtl)} />
|
{/* <CategoryDialog {...getDisplayDialogs(addDialogCtl)} />
|
||||||
<CategoryDialog {...getDisplayDialogs(editDialogCtl)} />
|
<CategoryDialog {...getDisplayDialogs(editDialogCtl)} />
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
{...deleteDialogCtl}
|
{...deleteDialogCtl}
|
||||||
@ -146,7 +145,7 @@ export const Categories = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
<Grid>
|
<Grid>
|
||||||
<PageHeader icon={FolderIcon} title={t("title.categories")}>
|
<PageHeader icon={FolderIcon} title={t("title.categories")}>
|
||||||
<Grid
|
<Grid
|
||||||
@ -166,7 +165,7 @@ export const Categories = () => {
|
|||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ float: "right" }}
|
sx={{ float: "right" }}
|
||||||
onClick={() => addDialogCtl.openDialog()}
|
onClick={() => dialogs.open(CategoryFormDialog, null)}
|
||||||
>
|
>
|
||||||
{t("button.add")}
|
{t("button.add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
217
frontend/src/contexts/dialogs.context.tsx
Normal file
217
frontend/src/contexts/dialogs.context.tsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* 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 * as React from "react";
|
||||||
|
|
||||||
|
export interface OpenDialogOptions<R> {
|
||||||
|
/**
|
||||||
|
* A function that is called before closing the dialog closes. The dialog
|
||||||
|
* stays open as long as the returned promise is not resolved. Use this if
|
||||||
|
* you want to perform an async action on close and show a loading state.
|
||||||
|
*
|
||||||
|
* @param result The result that the dialog will return after closing.
|
||||||
|
* @returns A promise that resolves when the dialog can be closed.
|
||||||
|
*/
|
||||||
|
onClose?: (result: R) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props that are passed to a dialog component.
|
||||||
|
*/
|
||||||
|
export interface DialogProps<P = undefined, R = void> {
|
||||||
|
/**
|
||||||
|
* The payload that was passed when the dialog was opened.
|
||||||
|
*/
|
||||||
|
payload: P;
|
||||||
|
/**
|
||||||
|
* Whether the dialog is open.
|
||||||
|
*/
|
||||||
|
open: boolean;
|
||||||
|
/**
|
||||||
|
* A function to call when the dialog should be closed. If the dialog has a return
|
||||||
|
* value, it should be passed as an argument to this function. You should use the promise
|
||||||
|
* that is returned to show a loading state while the dialog is performing async actions
|
||||||
|
* on close.
|
||||||
|
* @param result The result to return from the dialog.
|
||||||
|
* @returns A promise that resolves when the dialog can be fully closed.
|
||||||
|
*/
|
||||||
|
onClose: (result: R) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DialogComponent<P, R> = React.ComponentType<DialogProps<P, R>>;
|
||||||
|
|
||||||
|
export interface OpenDialog {
|
||||||
|
/**
|
||||||
|
* Open a dialog without payload.
|
||||||
|
* @param Component The dialog component to open.
|
||||||
|
* @param options Additional options for the dialog.
|
||||||
|
*/
|
||||||
|
<P extends undefined, R>(
|
||||||
|
Component: DialogComponent<P, R>,
|
||||||
|
payload?: P,
|
||||||
|
options?: OpenDialogOptions<R>,
|
||||||
|
): Promise<R>;
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
<P, R>(
|
||||||
|
Component: DialogComponent<P, R>,
|
||||||
|
payload: P,
|
||||||
|
options?: OpenDialogOptions<R>,
|
||||||
|
): Promise<R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
<R>(dialog: Promise<R>, result: R): Promise<R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<P, R> {
|
||||||
|
key: string;
|
||||||
|
open: boolean;
|
||||||
|
promise: Promise<R>;
|
||||||
|
Component: DialogComponent<P, R>;
|
||||||
|
payload: P;
|
||||||
|
onClose: (result: R) => Promise<void>;
|
||||||
|
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<DialogStackEntry<any, any>[]>([]);
|
||||||
|
const keyPrefix = React.useId();
|
||||||
|
const nextId = React.useRef(0);
|
||||||
|
const requestDialog = React.useCallback<OpenDialog>(
|
||||||
|
function open<P, R>(
|
||||||
|
Component: DialogComponent<P, R>,
|
||||||
|
payload: P,
|
||||||
|
options: OpenDialogOptions<R> = {},
|
||||||
|
) {
|
||||||
|
const { onClose = async () => {} } = options;
|
||||||
|
let resolve: ((result: R) => void) | undefined;
|
||||||
|
const promise = new Promise<R>((resolveImpl) => {
|
||||||
|
resolve = resolveImpl;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolve) {
|
||||||
|
throw new Error("resolve not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${keyPrefix}-${nextId.current}`;
|
||||||
|
|
||||||
|
nextId.current += 1;
|
||||||
|
|
||||||
|
const newEntry: DialogStackEntry<P, R> = {
|
||||||
|
key,
|
||||||
|
open: true,
|
||||||
|
promise,
|
||||||
|
Component,
|
||||||
|
payload,
|
||||||
|
onClose,
|
||||||
|
resolve,
|
||||||
|
};
|
||||||
|
|
||||||
|
setStack((prevStack) => [...prevStack, newEntry]);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
[keyPrefix],
|
||||||
|
);
|
||||||
|
const closeDialogUi = React.useCallback(
|
||||||
|
function closeDialogUi<R>(dialog: Promise<R>) {
|
||||||
|
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<R>(dialog: Promise<R>, 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 (
|
||||||
|
<DialogsContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
{stack.map(({ key, open, Component, payload, promise }) => (
|
||||||
|
<Component
|
||||||
|
key={key}
|
||||||
|
payload={payload}
|
||||||
|
open={open}
|
||||||
|
onClose={async (result) => {
|
||||||
|
await closeDialog(promise, result);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DialogsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DialogsProvider };
|
126
frontend/src/hooks/useDialogs.ts
Normal file
126
frontend/src/hooks/useDialogs.ts
Normal file
@ -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<OpenAlertDialog>(
|
||||||
|
// async (msg, { onClose, ...options } = {}) =>
|
||||||
|
// open(AlertDialog, { ...options, msg }, { onClose }),
|
||||||
|
// [open],
|
||||||
|
// );
|
||||||
|
// const confirm = React.useCallback<OpenConfirmDialog>(
|
||||||
|
// async (msg, { onClose, ...options } = {}) =>
|
||||||
|
// open(ConfirmDialog, { ...options, msg }, { onClose }),
|
||||||
|
// [open],
|
||||||
|
// );
|
||||||
|
// const prompt = React.useCallback<OpenPromptDialog>(
|
||||||
|
// async (msg, { onClose, ...options } = {}) =>
|
||||||
|
// open(PromptDialog, { ...options, msg }, { onClose }),
|
||||||
|
// [open],
|
||||||
|
// );
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
// alert,
|
||||||
|
// confirm,
|
||||||
|
// prompt,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
}),
|
||||||
|
// [alert, close, confirm, open, prompt],
|
||||||
|
[close, open],
|
||||||
|
);
|
||||||
|
}
|
@ -20,6 +20,7 @@ import { SnackbarCloseButton } from "@/app-components/displays/Toast/CloseButton
|
|||||||
import { ApiClientProvider } from "@/contexts/apiClient.context";
|
import { ApiClientProvider } from "@/contexts/apiClient.context";
|
||||||
import { AuthProvider } from "@/contexts/auth.context";
|
import { AuthProvider } from "@/contexts/auth.context";
|
||||||
import { ConfigProvider } from "@/contexts/config.context";
|
import { ConfigProvider } from "@/contexts/config.context";
|
||||||
|
import { DialogsProvider } from "@/contexts/dialogs.context";
|
||||||
import { PermissionProvider } from "@/contexts/permission.context";
|
import { PermissionProvider } from "@/contexts/permission.context";
|
||||||
import { SettingsProvider } from "@/contexts/setting.context";
|
import { SettingsProvider } from "@/contexts/setting.context";
|
||||||
import { ToastProvider } from "@/hooks/useToast";
|
import { ToastProvider } from "@/hooks/useToast";
|
||||||
@ -72,31 +73,33 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => {
|
|||||||
<main className={roboto.className}>
|
<main className={roboto.className}>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<ToastProvider
|
<DialogsProvider>
|
||||||
maxSnack={3}
|
<ToastProvider
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
maxSnack={3}
|
||||||
action={(snackbarKey) => (
|
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||||
<SnackbarCloseButton snackbarKey={snackbarKey} />
|
action={(snackbarKey) => (
|
||||||
)}
|
<SnackbarCloseButton snackbarKey={snackbarKey} />
|
||||||
>
|
)}
|
||||||
<StyledEngineProvider injectFirst>
|
>
|
||||||
<QueryClientProvider client={queryClient}>
|
<StyledEngineProvider injectFirst>
|
||||||
<CssBaseline />
|
<QueryClientProvider client={queryClient}>
|
||||||
<ApiClientProvider>
|
<CssBaseline />
|
||||||
<AuthProvider>
|
<ApiClientProvider>
|
||||||
<PermissionProvider>
|
<AuthProvider>
|
||||||
<SettingsProvider>
|
<PermissionProvider>
|
||||||
<SocketProvider>
|
<SettingsProvider>
|
||||||
{getLayout(<Component {...pageProps} />)}
|
<SocketProvider>
|
||||||
</SocketProvider>
|
{getLayout(<Component {...pageProps} />)}
|
||||||
</SettingsProvider>
|
</SocketProvider>
|
||||||
</PermissionProvider>
|
</SettingsProvider>
|
||||||
</AuthProvider>
|
</PermissionProvider>
|
||||||
</ApiClientProvider>
|
</AuthProvider>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
</ApiClientProvider>
|
||||||
</QueryClientProvider>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</StyledEngineProvider>
|
</QueryClientProvider>
|
||||||
</ToastProvider>
|
</StyledEngineProvider>
|
||||||
|
</ToastProvider>
|
||||||
|
</DialogsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</main>
|
</main>
|
||||||
|
Loading…
Reference in New Issue
Block a user