mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat(frontend): add useDialog
This commit is contained in:
parent
734838660a
commit
735e783864
@ -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],
|
||||
);
|
||||
|
||||
|
@ -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],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
|
173
frontend/src/types/common/dialogs.types.ts
Normal file
173
frontend/src/types/common/dialogs.types.ts
Normal 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>;
|
Loading…
Reference in New Issue
Block a user