1
0
mirror of https://github.com/hexastack/hexabot synced 2025-04-29 02:33:00 +00:00
hexabot/frontend/src/contexts/dialogs.context.tsx
2025-02-04 16:54:28 +01:00

148 lines
3.8 KiB
TypeScript

/*
* 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 {
createContext,
useCallback,
useId,
useMemo,
useRef,
useState,
} from "react";
import {
CloseDialog,
DialogComponent,
DialogProviderProps,
DialogStackEntry,
OpenDialog,
OpenDialogOptions,
} from "@/types/common/dialogs.types";
export const DialogsContext = createContext<
| {
open: OpenDialog;
close: CloseDialog;
}
| undefined
>(undefined);
/**
* 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] = useState<DialogStackEntry<any, any>[]>([]);
const keyPrefix = useId();
const nextId = useRef(0);
const requestDialog = 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,
msgProps: { count: options.count, mode: options.mode },
};
setStack((prevStack) => [...prevStack, newEntry]);
return promise;
},
[keyPrefix],
);
const closeDialogUi = 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 = 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 = useMemo(
() => ({
open: requestDialog,
close: closeDialog,
}),
[requestDialog, closeDialog],
);
return (
<DialogsContext.Provider value={contextValue}>
{children}
{stack.map(({ key, open, Component, payload, promise, msgProps }) => (
<Component
key={key}
payload={payload}
open={open}
onClose={async (result) => {
await closeDialog(promise, result);
}}
{...msgProps}
/>
))}
</DialogsContext.Provider>
);
}
export { DialogsProvider };