feat(frontend): add useDialog

This commit is contained in:
yassinedorbozgithub 2025-02-01 10:01:00 +01:00
parent 734838660a
commit 735e783864
4 changed files with 250 additions and 208 deletions

View File

@ -6,106 +6,34 @@
* 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). * 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"; import {
createContext,
FC,
useCallback,
useId,
useMemo,
useRef,
useState,
} from "react";
export interface OpenDialogOptions<R> { import { ConfirmDialog, ConfirmDialogProps } from "@/app-components/dialogs";
/** import {
* A function that is called before closing the dialog closes. The dialog CloseDialog,
* stays open as long as the returned promise is not resolved. Use this if DialogComponent,
* you want to perform an async action on close and show a loading state. DialogProviderProps,
* DialogStackEntry,
* @param result The result that the dialog will return after closing. OpenDialog,
* @returns A promise that resolves when the dialog can be closed. OpenDialogOptions,
*/ } from "@/types/common/dialogs.types";
onClose?: (result: R) => Promise<void>;
}
/** export const DialogsContext = createContext<
* The props that are passed to a dialog component. | {
*/ open: OpenDialog;
export interface DialogProps<P = undefined, R = void> { close: CloseDialog;
/** confirm: FC<ConfirmDialogProps>;
* The payload that was passed when the dialog was opened. }
*/ | undefined
payload: P; >(undefined);
/**
* 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 * Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to
@ -121,10 +49,10 @@ export interface DialogProviderProps {
*/ */
function DialogsProvider(props: DialogProviderProps) { function DialogsProvider(props: DialogProviderProps) {
const { children, unmountAfter = 1000 } = props; const { children, unmountAfter = 1000 } = props;
const [stack, setStack] = React.useState<DialogStackEntry<any, any>[]>([]); const [stack, setStack] = useState<DialogStackEntry<any, any>[]>([]);
const keyPrefix = React.useId(); const keyPrefix = useId();
const nextId = React.useRef(0); const nextId = useRef(0);
const requestDialog = React.useCallback<OpenDialog>( const requestDialog = useCallback<OpenDialog>(
function open<P, R>( function open<P, R>(
Component: DialogComponent<P, R>, Component: DialogComponent<P, R>,
payload: P, payload: P,
@ -160,7 +88,7 @@ function DialogsProvider(props: DialogProviderProps) {
}, },
[keyPrefix], [keyPrefix],
); );
const closeDialogUi = React.useCallback( const closeDialogUi = useCallback(
function closeDialogUi<R>(dialog: Promise<R>) { function closeDialogUi<R>(dialog: Promise<R>) {
setStack((prevStack) => setStack((prevStack) =>
prevStack.map((entry) => prevStack.map((entry) =>
@ -176,7 +104,7 @@ function DialogsProvider(props: DialogProviderProps) {
}, },
[unmountAfter], [unmountAfter],
); );
const closeDialog = React.useCallback( const closeDialog = useCallback(
async function closeDialog<R>(dialog: Promise<R>, result: R) { async function closeDialog<R>(dialog: Promise<R>, result: R) {
const entryToClose = stack.find((entry) => entry.promise === dialog); const entryToClose = stack.find((entry) => entry.promise === dialog);
@ -192,8 +120,12 @@ function DialogsProvider(props: DialogProviderProps) {
}, },
[stack, closeDialogUi], [stack, closeDialogUi],
); );
const contextValue = React.useMemo( const contextValue = useMemo(
() => ({ open: requestDialog, close: closeDialog }), () => ({
open: requestDialog,
close: closeDialog,
confirm: ConfirmDialog,
}),
[requestDialog, closeDialog], [requestDialog, closeDialog],
); );

View File

@ -6,121 +6,56 @@
* 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). * 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 React, { useContext, useMemo } from "react";
import { ConfirmDialog } from "@/app-components/dialogs";
import { DialogsContext } from "@/contexts/dialogs.context";
import { import {
CloseDialog, CloseDialog,
DialogsContext, OpenConfirmDialog,
OpenDialog, OpenDialog,
} from "@/contexts/dialogs.context"; } from "@/types/common/dialogs.types";
/*
* 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 { export interface DialogHook {
// alert: OpenAlertDialog;
// confirm: OpenConfirmDialog;
// prompt: OpenPromptDialog;
open: OpenDialog; open: OpenDialog;
close: CloseDialog; close: CloseDialog;
// alert: OpenAlertDialog;
// prompt: OpenPromptDialog;
confirm: OpenConfirmDialog;
} }
export function useDialogs(): DialogHook { export const useDialogs = (): DialogHook => {
const { open, close } = useContext(DialogsContext); const context = useContext(DialogsContext);
if (!context) {
throw new Error("useDialogs must be used within a DialogsProvider");
}
const { open, close } = context;
// const alert = React.useCallback<OpenAlertDialog>( // const alert = React.useCallback<OpenAlertDialog>(
// async (msg, { onClose, ...options } = {}) => // async (msg, { onClose, ...options } = {}) =>
// open(AlertDialog, { ...options, msg }, { onClose }), // open(AlertDialog, { ...options, msg }, { onClose }),
// [open], // [open],
// ); // );
// const confirm = React.useCallback<OpenConfirmDialog>(
// async (msg, { onClose, ...options } = {}) =>
// open(ConfirmDialog, { ...options, msg }, { onClose }),
// [open],
// );
// const prompt = React.useCallback<OpenPromptDialog>( // const prompt = React.useCallback<OpenPromptDialog>(
// async (msg, { onClose, ...options } = {}) => // async (msg, { onClose, ...options } = {}) =>
// open(PromptDialog, { ...options, msg }, { onClose }), // open(PromptDialog, { ...options, msg }, { onClose }),
// [open], // [open],
// ); // );
const confirm = React.useCallback<OpenConfirmDialog>(
async (msg, { onClose, ...options } = {}) =>
open(ConfirmDialog, { ...options, msg }, { onClose }),
[open],
);
return useMemo( return useMemo(
() => ({ () => ({
// alert,
// confirm,
// prompt,
open, open,
close, close,
// alert,
// prompt,
confirm,
}), }),
// [alert, close, confirm, open, prompt], [close, open, confirm],
[close, open],
); );
} };

View File

@ -86,17 +86,19 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<CssBaseline /> <CssBaseline />
<ApiClientProvider> <ApiClientProvider>
<BroadcastChannelProvider channelName="main-channel"> <DialogsProvider>
<AuthProvider> <BroadcastChannelProvider channelName="main-channel">
<PermissionProvider> <AuthProvider>
<SettingsProvider> <PermissionProvider>
<SocketProvider> <SettingsProvider>
{getLayout(<Component {...pageProps} />)} <SocketProvider>
</SocketProvider> {getLayout(<Component {...pageProps} />)}
</SettingsProvider> </SocketProvider>
</PermissionProvider> </SettingsProvider>
</AuthProvider> </PermissionProvider>
</BroadcastChannelProvider> </AuthProvider>
</BroadcastChannelProvider>
</DialogsProvider>
</ApiClientProvider> </ApiClientProvider>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider> </QueryClientProvider>

View File

@ -0,0 +1,173 @@
/*
* 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 { DialogProps as MuiDialogProps } from "@mui/material";
import { BaseSyntheticEvent, ReactNode } from "react";
// context
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 ConfirmOptions extends OpenDialogOptions<boolean> {
/**
* A title for the dialog. Defaults to `'Confirm'`.
*/
title?: React.ReactNode;
/**
* The text to show in the "Ok" button. Defaults to `'Ok'`.
*/
okText?: React.ReactNode;
/**
* Denotes the purpose of the dialog. This will affect the color of the
* "Ok" button. Defaults to `undefined`.
*/
severity?: "error" | "info" | "success" | "warning";
/**
* The text to show in the "Cancel" button. Defaults to `'Cancel'`.
*/
cancelText?: React.ReactNode;
}
export interface OpenConfirmDialog {
/**
* Open a confirmation dialog. Returns a promise that resolves to true if
* the user confirms, false if the user cancels.
*
* @param msg The message to show in the dialog.
* @param options Additional options for the dialog.
* @returns A promise that resolves to true if the user confirms, false if the user cancels.
*/
(msg: React.ReactNode, options?: ConfirmOptions): Promise<boolean>;
}
export interface DialogHook {
// alert: OpenAlertDialog;
confirm: OpenConfirmDialog;
// prompt: OpenPromptDialog;
open: OpenDialog;
close: CloseDialog;
}
export 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;
}
// form dialog
export interface FormDialogProps<T> extends MuiDialogProps {
title: string;
children: ReactNode;
onConfirm: (
e: BaseSyntheticEvent<object, any, any>,
) => Promise<T | undefined>;
}
// form
export type ComponentFormProps<T> = {
data: T | null;
onError?: () => void;
onSuccess?: () => void;
};
export interface FormButtonsProps<T> {
onCancel?: () => void;
onConfirm?: (
e: BaseSyntheticEvent<object, any, any>,
) => Promise<T | undefined>;
}
export type HTMLFormElementExtra<T> = {
submitAsync: (
e: BaseSyntheticEvent<object, any, any>,
) => Promise<T | undefined>;
};
export type HTMLFormElementExtension<T> =
| HTMLFormElement
| HTMLFormElementExtra<T>;
export type ComponentFormDialogProps<T> = DialogProps<T | null, boolean>;