From d412635f6b63f2a183cbd2e6ee2e83aaf454144f Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Wed, 27 Mar 2024 13:30:30 +0000 Subject: [PATCH] feat(component): new hook to open confirm modal (#6342) new exports from `@affine/component`: ```ts import { ConfirmModalProvider, useConfirmModal } from "@affine/component" ``` Open confirm modal with hook: ```ts const Component = () => { const { openConfirmModal } = useConformModal(); const open = () => { openConfirmModal({ // props of ConfirmModal /** * will show loading state when confirm clicked, and close after onConfirm finished */ onConfirm: async () => { await new Promise((r) => setTimeout(r, 2000)); }, }); } return } ``` --- .../frontend/component/.storybook/preview.tsx | 9 +- .../src/ui/modal/confirm-modal.stories.tsx | 38 ++++++++ .../component/src/ui/modal/confirm-modal.tsx | 89 ++++++++++++++++++- .../component/src/ui/modal/styles.css.ts | 4 + .../enable-affine-cloud-modal/index.tsx | 2 +- .../general-setting/plans/modals.tsx | 2 +- .../delete-leave-workspace/delete/index.tsx | 1 + 7 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 packages/frontend/component/src/ui/modal/confirm-modal.stories.tsx diff --git a/packages/frontend/component/.storybook/preview.tsx b/packages/frontend/component/.storybook/preview.tsx index e255d0ea09fb..8cd3ebd6aabe 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 000000000000..f3b58e5b9cfa --- /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 35a4093bdc52..349056ca8b8d 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 b3a8d44e3c22..6372d9bef593 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 bd33049e69ed..c1efc7c1c9bd 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 9df712e6b33a..ea613c5d5bf7 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 51542bdf395d..0631ea444170 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 = ({