diff --git a/packages/frontend/component/.storybook/preview.tsx b/packages/frontend/component/.storybook/preview.tsx index e255d0ea0..8cd3ebd6a 100644 --- a/packages/frontend/component/.storybook/preview.tsx +++ b/packages/frontend/component/.storybook/preview.tsx @@ -8,6 +8,7 @@ import { useDarkMode } from 'storybook-dark-mode'; import type { Preview } from '@storybook/react'; import React from 'react'; +import { ConfirmModalProvider } from '../src/ui/modal/confirm-modal'; export const parameters: Preview = { argTypes: { @@ -53,9 +54,11 @@ export const decorators = [ (Story: ComponentType, context) => { return ( - - - + + + + + ); }, diff --git a/packages/frontend/component/src/ui/modal/confirm-modal.stories.tsx b/packages/frontend/component/src/ui/modal/confirm-modal.stories.tsx new file mode 100644 index 000000000..f3b58e5b9 --- /dev/null +++ b/packages/frontend/component/src/ui/modal/confirm-modal.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta } from '@storybook/react'; + +import { Button } from '../button'; +import { + ConfirmModal, + type ConfirmModalProps, + useConfirmModal, +} from './confirm-modal'; + +export default { + title: 'UI/Modal/Confirm Modal', + component: ConfirmModal, + argTypes: {}, +} satisfies Meta; + +export const UsingHook = () => { + const { openConfirmModal } = useConfirmModal(); + + const onConfirm = () => + new Promise(resolve => setTimeout(resolve, 2000)); + + const showConfirm = () => { + openConfirmModal({ + cancelText: 'Cancel', + confirmButtonOptions: { + children: 'Confirm', + }, + title: 'Confirm Modal', + children: 'Are you sure you want to confirm?', + onConfirm, + onCancel: () => { + console.log('Cancelled'); + }, + }); + }; + + return ; +}; diff --git a/packages/frontend/component/src/ui/modal/confirm-modal.tsx b/packages/frontend/component/src/ui/modal/confirm-modal.tsx index 35a4093bd..349056ca8 100644 --- a/packages/frontend/component/src/ui/modal/confirm-modal.tsx +++ b/packages/frontend/component/src/ui/modal/confirm-modal.tsx @@ -1,5 +1,7 @@ import { DialogTrigger } from '@radix-ui/react-dialog'; import clsx from 'clsx'; +import type { PropsWithChildren } from 'react'; +import { createContext, useCallback, useContext, useState } from 'react'; import type { ButtonProps } from '../button'; import { Button } from '../button'; @@ -9,9 +11,11 @@ import * as styles from './styles.css'; export interface ConfirmModalProps extends ModalProps { confirmButtonOptions?: ButtonProps; - onConfirm?: () => void; + onConfirm?: (() => void) | (() => Promise); + onCancel?: () => void; cancelText?: string; cancelButtonOptions?: ButtonProps; + reverseFooter?: boolean; } export const ConfirmModal = ({ @@ -20,7 +24,9 @@ export const ConfirmModal = ({ // FIXME: we need i18n cancelText = 'Cancel', cancelButtonOptions, + reverseFooter, onConfirm, + onCancel, width = 480, ...props }: ConfirmModalProps) => { @@ -36,13 +42,92 @@ export const ConfirmModal = ({
- +
); }; + +interface ConfirmModalContextProps { + modalProps: ConfirmModalProps; + openConfirmModal: (props?: ConfirmModalProps) => void; + closeConfirmModal: () => void; +} +const ConfirmModalContext = createContext({ + modalProps: { open: false }, + openConfirmModal: () => {}, + closeConfirmModal: () => {}, +}); +export const ConfirmModalProvider = ({ children }: PropsWithChildren) => { + const [modalProps, setModalProps] = useState({ + open: false, + }); + + const setLoading = useCallback((value: boolean) => { + setModalProps(prev => ({ + ...prev, + confirmButtonOptions: { + ...prev.confirmButtonOptions, + loading: value, + }, + })); + }, []); + + const closeConfirmModal = useCallback(() => { + setModalProps({ open: false }); + }, []); + + const openConfirmModal = useCallback( + (props?: ConfirmModalProps) => { + if (!props) { + setModalProps({ open: true }); + return; + } + + const { onConfirm: _onConfirm, ...otherProps } = props; + + const onConfirm = () => { + setLoading(true); + _onConfirm?.() + ?.catch(console.error) + ?.finally(() => closeConfirmModal()); + }; + setModalProps({ ...otherProps, onConfirm, open: true }); + }, + [closeConfirmModal, setLoading] + ); + + const onOpenChange = useCallback((open: boolean) => { + setModalProps(props => ({ ...props, open })); + }, []); + + return ( + + {children} + {/* TODO: multi-instance support(unnecessary for now) */} + + + ); +}; +export const useConfirmModal = () => { + const context = useContext(ConfirmModalContext); + if (!context) { + throw new Error( + 'useConfirmModal must be used within a ConfirmModalProvider' + ); + } + return { + openConfirmModal: context.openConfirmModal, + closeConfirmModal: context.closeConfirmModal, + }; +}; diff --git a/packages/frontend/component/src/ui/modal/styles.css.ts b/packages/frontend/component/src/ui/modal/styles.css.ts index b3a8d44e3..6372d9bef 100644 --- a/packages/frontend/component/src/ui/modal/styles.css.ts +++ b/packages/frontend/component/src/ui/modal/styles.css.ts @@ -65,6 +65,10 @@ export const modalFooter = style({ '&.modalFooterWithChildren': { paddingTop: '20px', }, + '&.reverse': { + flexDirection: 'row-reverse', + justifyContent: 'flex-start', + }, }, }); export const confirmModalContent = style({ diff --git a/packages/frontend/core/src/components/affine/enable-affine-cloud-modal/index.tsx b/packages/frontend/core/src/components/affine/enable-affine-cloud-modal/index.tsx index bd33049e6..c1efc7c1c 100644 --- a/packages/frontend/core/src/components/affine/enable-affine-cloud-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/enable-affine-cloud-modal/index.tsx @@ -11,7 +11,7 @@ import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-s export const EnableAffineCloudModal = ({ onConfirm: propsOnConfirm, ...props -}: ConfirmModalProps) => { +}: Omit & { onConfirm: () => void }) => { const t = useAFFiNEI18N(); const loginStatus = useCurrentLoginStatus(); const setAuthAtom = useSetAtom(authAtom); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx index 9df712e6b..ea613c5d5 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/modals.tsx @@ -55,7 +55,7 @@ export const ConfirmLoadingModal = ({ onOpenChange={onOpenChange} onConfirm={() => { confirmed.current = true; - onConfirm?.(); + onConfirm?.()?.catch(console.error); }} {...props} > diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx index 51542bdf3..0631ea444 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx @@ -13,6 +13,7 @@ import * as styles from './style.css'; interface WorkspaceDeleteProps extends ConfirmModalProps { workspaceMetadata: WorkspaceMetadata; + onConfirm?: () => void; } export const WorkspaceDeleteModal = ({