2363 refactor the dialog component to use the new scope architecture (#2415)

* create Scope

* refactor dialog manager

* finished refactoring

* modify closeDialog to use a recoilcallback

* modify according to comments + add effet component

* fix spreadsheet import stories
This commit is contained in:
bosiraphael
2023-11-10 12:36:25 +01:00
committed by GitHub
parent 6a700ad1a5
commit e0289ba9f2
22 changed files with 249 additions and 152 deletions

View File

@ -7,7 +7,8 @@ import { RecoilRoot } from 'recoil';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { DialogProvider } from '@/ui/feedback/dialog/components/DialogProvider';
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
import { SnackBarProvider } from '@/ui/feedback/snack-bar/components/SnackBarProvider';
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
import { ThemeType } from '@/ui/theme/constants/theme';
@ -38,11 +39,13 @@ root.render(
<PageChangeEffect />
<AppThemeProvider>
<SnackBarProvider>
<DialogProvider>
<StrictMode>
<App />
</StrictMode>
</DialogProvider>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManager>
<StrictMode>
<App />
</StrictMode>
</DialogManager>
</DialogManagerScope>
</SnackBarProvider>
</AppThemeProvider>
</ApolloMetadataClientProvider>

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { IconX } from '@/ui/display/icon/index';
import { useDialog } from '@/ui/feedback/dialog//hooks/useDialog';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
import { IconButton } from '@/ui/input/button/components/IconButton';
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
@ -33,7 +33,7 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProps) => {
initialStep,
});
const { enqueueDialog } = useDialog();
const { enqueueDialog } = useDialogManager();
const handleClose = () => {
if (activeStep === -1) {

View File

@ -11,7 +11,7 @@ import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableDat
import { setColumn } from '@/spreadsheet-import/utils/setColumn';
import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
import { useDialog } from '@/ui/feedback/dialog//hooks/useDialog';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import { Modal } from '@/ui/layout/modal/components/Modal';
@ -112,7 +112,7 @@ export const MatchColumnsStep = <T extends string>({
headerValues,
onContinue,
}: MatchColumnsStepProps<T>) => {
const { enqueueDialog } = useDialog();
const { enqueueDialog } = useDialogManager();
const { enqueueSnackBar } = useSnackBar();
const dataExample = data.slice(0, 2);
const { fields, autoMapHeaders, autoMapDistance } =

View File

@ -9,7 +9,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre
import { Data } from '@/spreadsheet-import/types';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { IconTrash } from '@/ui/display/icon';
import { useDialog } from '@/ui/feedback/dialog//hooks/useDialog';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
import { Button } from '@/ui/input/button/components/Button';
import { Toggle } from '@/ui/input/components/Toggle';
import { Modal } from '@/ui/layout/modal/components/Modal';
@ -69,7 +69,7 @@ export const ValidationStep = <T extends string>({
file,
onSubmitStart,
}: ValidationStepProps<T>) => {
const { enqueueDialog } = useDialog();
const { enqueueDialog } = useDialogManager();
const { fields, onClose, onSubmit, rowHook, tableHook } =
useSpreadsheetImportInternal<T>();

View File

@ -4,6 +4,7 @@ import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/Providers';
import { MatchColumnsStep } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
const meta: Meta<typeof MatchColumnsStep> = {
title: 'Modules/SpreadsheetImport/MatchColumnsStep',
@ -57,13 +58,15 @@ const mockData = [
];
export const Default = () => (
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<MatchColumnsStep
headerValues={mockData[0] as string[]}
data={mockData.slice(1)}
onContinue={() => null}
/>
</ModalWrapper>
</Providers>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<MatchColumnsStep
headerValues={mockData[0] as string[]}
data={mockData.slice(1)}
onContinue={() => null}
/>
</ModalWrapper>
</Providers>
</DialogManagerScope>
);

View File

@ -7,6 +7,7 @@ import {
headerSelectionTableFields,
mockRsiValues,
} from '@/spreadsheet-import/tests/mockRsiValues';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
const meta: Meta<typeof SelectHeaderStep> = {
title: 'Modules/SpreadsheetImport/SelectHeaderStep',
@ -19,12 +20,14 @@ const meta: Meta<typeof SelectHeaderStep> = {
export default meta;
export const Default = () => (
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SelectHeaderStep
data={headerSelectionTableFields}
onContinue={() => Promise.resolve()}
/>
</ModalWrapper>
</Providers>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SelectHeaderStep
data={headerSelectionTableFields}
onContinue={() => Promise.resolve()}
/>
</ModalWrapper>
</Providers>
</DialogManagerScope>
);

View File

@ -4,6 +4,7 @@ import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/Providers';
import { SelectSheetStep } from '@/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep';
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
const meta: Meta<typeof SelectSheetStep> = {
title: 'Modules/SpreadsheetImport/SelectSheetStep',
@ -18,12 +19,14 @@ export default meta;
const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3'];
export const Default = () => (
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SelectSheetStep
sheetNames={sheetNames}
onContinue={() => Promise.resolve()}
/>
</ModalWrapper>
</Providers>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SelectSheetStep
sheetNames={sheetNames}
onContinue={() => Promise.resolve()}
/>
</ModalWrapper>
</Providers>
</DialogManagerScope>
);

View File

@ -4,6 +4,7 @@ import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/Providers';
import { UploadStep } from '@/spreadsheet-import/steps/components/UploadStep/UploadStep';
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
const meta: Meta<typeof UploadStep> = {
title: 'Modules/SpreadsheetImport/UploadStep',
@ -16,9 +17,11 @@ const meta: Meta<typeof UploadStep> = {
export default meta;
export const Default = () => (
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<UploadStep onContinue={() => Promise.resolve()} />
</ModalWrapper>
</Providers>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<UploadStep onContinue={() => Promise.resolve()} />
</ModalWrapper>
</Providers>
</DialogManagerScope>
);

View File

@ -7,6 +7,7 @@ import {
editableTableInitialData,
mockRsiValues,
} from '@/spreadsheet-import/tests/mockRsiValues';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
const meta: Meta<typeof ValidationStep> = {
title: 'Modules/SpreadsheetImport/ValidationStep',
@ -21,9 +22,11 @@ export default meta;
const file = new File([''], 'file.csv');
export const Default = () => (
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<ValidationStep initialData={editableTableInitialData} file={file} />
</ModalWrapper>
</Providers>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<ValidationStep initialData={editableTableInitialData} file={file} />
</ModalWrapper>
</Providers>
</DialogManagerScope>
);

View File

@ -0,0 +1,24 @@
import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates';
import { useDialogManager } from '../hooks/useDialogManager';
import { Dialog } from './Dialog';
import { DialogManagerEffect } from './DialogManagerEffect';
export const DialogManager = ({ children }: React.PropsWithChildren) => {
const { dialogInternal } = useDialogManagerScopedStates();
const { closeDialog } = useDialogManager();
return (
<>
<DialogManagerEffect />
{children}
{dialogInternal.queue.map(({ buttons, children, id, message, title }) => (
<Dialog
key={id}
{...{ title, message, buttons, id, children }}
onClose={() => closeDialog(id)}
/>
))}
</>
);
};

View File

@ -0,0 +1,22 @@
import { useEffect } from 'react';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates';
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
export const DialogManagerEffect = () => {
const { dialogInternal } = useDialogManagerScopedStates();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
useEffect(() => {
if (dialogInternal.queue.length === 0) {
return;
}
setHotkeyScopeAndMemorizePreviousScope(DialogHotkeyScope.Dialog);
}, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]);
return <></>;
};

View File

@ -0,0 +1,25 @@
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { DialogManagerScopeInternalContext } from '../../scopes/scope-internal-context/DialogManagerScopeInternalContext';
import { dialogInternalScopedState } from '../../states/dialogInternalScopedState';
type useDialogManagerScopedStatesProps = {
dialogManagerScopeId?: string;
};
export const useDialogManagerScopedStates = (
props?: useDialogManagerScopedStatesProps,
) => {
const scopeId = useAvailableScopeIdOrThrow(
DialogManagerScopeInternalContext,
props?.dialogManagerScopeId,
);
const [dialogInternal, setDialogInternal] = useRecoilScopedStateV2(
dialogInternalScopedState,
scopeId,
);
return { dialogInternal, setDialogInternal };
};

View File

@ -0,0 +1,62 @@
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { DialogManagerScopeInternalContext } from '../scopes/scope-internal-context/DialogManagerScopeInternalContext';
import { dialogInternalScopedState } from '../states/dialogInternalScopedState';
import { DialogOptions } from '../types/DialogOptions';
type useDialogManagerProps = {
dialogManagerScopeId?: string;
};
export const useDialogManager = (props?: useDialogManagerProps) => {
const scopeId = useAvailableScopeIdOrThrow(
DialogManagerScopeInternalContext,
props?.dialogManagerScopeId,
);
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const closeDialog = useRecoilCallback(
({ set }) =>
(id: string) => {
set(dialogInternalScopedState({ scopeId: scopeId }), (prevState) => ({
...prevState,
queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
}));
goBackToPreviousHotkeyScope();
},
[goBackToPreviousHotkeyScope, scopeId],
);
const setDialogQueue = useRecoilCallback(
({ set }) =>
(newValue) =>
set(dialogInternalScopedState({ scopeId: scopeId }), (prev) => {
if (prev.queue.length >= prev.maxQueue) {
return {
...prev,
queue: [...prev.queue.slice(1), newValue] as DialogOptions[],
};
}
return {
...prev,
queue: [...prev.queue, newValue] as DialogOptions[],
};
}),
[scopeId],
);
const enqueueDialog = (options?: Omit<DialogOptions, 'id'>) => {
setDialogQueue({
id: v4(),
...options,
});
};
return { closeDialog, enqueueDialog };
};

View File

@ -0,0 +1,23 @@
import { ReactNode } from 'react';
import { DialogManagerScopeInternalContext } from './scope-internal-context/DialogManagerScopeInternalContext';
type DialogManagerScopeProps = {
children: ReactNode;
dialogManagerScopeId: string;
};
export const DialogManagerScope = ({
children,
dialogManagerScopeId,
}: DialogManagerScopeProps) => {
return (
<DialogManagerScopeInternalContext.Provider
value={{
scopeId: dialogManagerScopeId,
}}
>
{children}
</DialogManagerScopeInternalContext.Provider>
);
};

View File

@ -0,0 +1,7 @@
import { ScopedStateKey } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopedStateKey';
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
type DialogManagerScopeInternalContextProps = ScopedStateKey;
export const DialogManagerScopeInternalContext =
createScopeInternalContext<DialogManagerScopeInternalContextProps>();

View File

@ -0,0 +1,16 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
import { DialogOptions } from '../types/DialogOptions';
type DialogState = {
maxQueue: number;
queue: DialogOptions[];
};
export const dialogInternalScopedState = createScopedState<DialogState>({
key: 'dialog/internal-state',
defaultValue: {
maxQueue: 2,
queue: [],
},
});

View File

@ -0,0 +1,5 @@
import { DialogProps } from '../components/Dialog';
export type DialogOptions = DialogProps & {
id: string;
};

View File

@ -1,49 +0,0 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { dialogInternalState } from '../states/dialogState';
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
import { Dialog } from './Dialog';
export const DialogProvider = ({ children }: React.PropsWithChildren) => {
const [dialogInternal, setDialogInternal] =
useRecoilState(dialogInternalState);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
// Handle dialog close event
const handleDialogClose = (id: string) => {
setDialogInternal((prevState) => ({
...prevState,
queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
}));
goBackToPreviousHotkeyScope();
};
useEffect(() => {
if (dialogInternal.queue.length === 0) {
return;
}
setHotkeyScopeAndMemorizePreviousScope(DialogHotkeyScope.Dialog);
}, [dialogInternal.queue, setHotkeyScopeAndMemorizePreviousScope]);
return (
<>
{children}
{dialogInternal.queue.map(({ buttons, children, id, message, title }) => (
<Dialog
key={id}
{...{ title, message, buttons, id, children }}
onClose={() => handleDialogClose(id)}
/>
))}
</>
);
};

View File

@ -1,17 +0,0 @@
import { useSetRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { DialogOptions, dialogSetQueueState } from '../states/dialogState';
export const useDialog = () => {
const setDialogQueue = useSetRecoilState(dialogSetQueueState);
const enqueueDialog = (options?: Omit<DialogOptions, 'id'>) => {
setDialogQueue({
id: uuidv4(),
...options,
});
};
return { enqueueDialog };
};

View File

@ -1,39 +0,0 @@
import { atom, selector } from 'recoil';
import { DialogProps } from '../components/Dialog';
export type DialogOptions = DialogProps & {
id: string;
};
export type DialogState = {
maxQueue: number;
queue: DialogOptions[];
};
export const dialogInternalState = atom<DialogState>({
key: 'dialog/internal-state',
default: {
maxQueue: 2,
queue: [],
},
});
export const dialogSetQueueState = selector<DialogOptions | null>({
key: 'dialog/queue-state',
get: ({ get: _get }) => null, // We don't care about getting the value
set: ({ set }, newValue) =>
set(dialogInternalState, (prev) => {
if (prev.queue.length >= prev.maxQueue) {
return {
...prev,
queue: [...prev.queue.slice(1), newValue] as DialogOptions[],
};
}
return {
...prev,
queue: [...prev.queue, newValue] as DialogOptions[],
};
}),
});