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).
*/
import * as React from "react";
import {
createContext,
FC,
useCallback,
useId,
useMemo,
useRef,
useState,
} 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>;
}
import { ConfirmDialog, ConfirmDialogProps } from "@/app-components/dialogs";
import {
CloseDialog,
DialogComponent,
DialogProviderProps,
DialogStackEntry,
OpenDialog,
OpenDialogOptions,
} from "@/types/common/dialogs.types";
/**
* 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;
}
export const DialogsContext = createContext<
| {
open: OpenDialog;
close: CloseDialog;
confirm: FC<ConfirmDialogProps>;
}
| undefined
>(undefined);
/**
* 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) {
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>(
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,
@ -160,7 +88,7 @@ function DialogsProvider(props: DialogProviderProps) {
},
[keyPrefix],
);
const closeDialogUi = React.useCallback(
const closeDialogUi = useCallback(
function closeDialogUi<R>(dialog: Promise<R>) {
setStack((prevStack) =>
prevStack.map((entry) =>
@ -176,7 +104,7 @@ function DialogsProvider(props: DialogProviderProps) {
},
[unmountAfter],
);
const closeDialog = React.useCallback(
const closeDialog = useCallback(
async function closeDialog<R>(dialog: Promise<R>, result: R) {
const entryToClose = stack.find((entry) => entry.promise === dialog);
@ -192,8 +120,12 @@ function DialogsProvider(props: DialogProviderProps) {
},
[stack, closeDialogUi],
);
const contextValue = React.useMemo(
() => ({ open: requestDialog, close: closeDialog }),
const contextValue = useMemo(
() => ({
open: requestDialog,
close: closeDialog,
confirm: ConfirmDialog,
}),
[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).
*/
import { useContext, useMemo } from "react";
import React, { useContext, useMemo } from "react";
import { ConfirmDialog } from "@/app-components/dialogs";
import { DialogsContext } from "@/contexts/dialogs.context";
import {
CloseDialog,
DialogsContext,
OpenConfirmDialog,
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).
*/
} from "@/types/common/dialogs.types";
export interface DialogHook {
// alert: OpenAlertDialog;
// confirm: OpenConfirmDialog;
// prompt: OpenPromptDialog;
open: OpenDialog;
close: CloseDialog;
// alert: OpenAlertDialog;
// prompt: OpenPromptDialog;
confirm: OpenConfirmDialog;
}
export function useDialogs(): DialogHook {
const { open, close } = useContext(DialogsContext);
export const useDialogs = (): DialogHook => {
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>(
// 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],
// );
const confirm = React.useCallback<OpenConfirmDialog>(
async (msg, { onClose, ...options } = {}) =>
open(ConfirmDialog, { ...options, msg }, { onClose }),
[open],
);
return useMemo(
() => ({
// alert,
// confirm,
// prompt,
open,
close,
// alert,
// prompt,
confirm,
}),
// [alert, close, confirm, open, prompt],
[close, open],
[close, open, confirm],
);
}
};

View File

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