Refactor dialog old component states (#13186)

This PR refactors dialog old component state management.
This commit is contained in:
Lucas Bordeau
2025-07-15 11:32:16 +02:00
committed by GitHub
parent 1a81e43286
commit 3698c683db
17 changed files with 88 additions and 153 deletions

View File

@ -51,7 +51,7 @@ export const AppRouterProviders = () => {
<UserThemeProviderEffect />
<SnackBarProvider>
<ErrorMessageEffect />
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManagerScope dialogComponentInstanceId="dialog-manager">
<DialogManager>
<StrictMode>
<PromiseRejectionEffect />

View File

@ -62,7 +62,7 @@ const mockData = [
];
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManagerScope dialogComponentInstanceId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="match-columns-step"

View File

@ -40,7 +40,7 @@ const meta: Meta<typeof SelectHeaderStep> = {
export default meta;
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManagerScope dialogComponentInstanceId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="select-header-step"

View File

@ -40,7 +40,7 @@ export default meta;
const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3'];
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManagerScope dialogComponentInstanceId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="select-sheet-step"

View File

@ -44,7 +44,7 @@ const meta: Meta<typeof UploadStep> = {
export default meta;
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManagerScope dialogComponentInstanceId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper modalId="upload-step" onClose={() => null}>
<UploadStep

View File

@ -43,7 +43,7 @@ export default meta;
const file = new File([''], 'file.csv');
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManagerScope dialogComponentInstanceId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="validation-step"

View File

@ -1,11 +1,14 @@
import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates';
import { dialogInternalComponentState } from '@/ui/feedback/dialog-manager/states/dialogInternalComponentState';
import { useDialogManager } from '../hooks/useDialogManager';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Dialog } from './Dialog';
import { DialogManagerEffect } from './DialogManagerEffect';
export const DialogManager = ({ children }: React.PropsWithChildren) => {
const { dialogInternal } = useDialogManagerScopedStates();
const dialogInternal = useRecoilComponentValueV2(
dialogInternalComponentState,
);
const { closeDialog } = useDialogManager();
return (

View File

@ -3,10 +3,14 @@ import { useEffect } from 'react';
import { DIALOG_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useDialogManagerScopedStates } from '../hooks/internal/useDialogManagerScopedStates';
import { dialogInternalComponentState } from '@/ui/feedback/dialog-manager/states/dialogInternalComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const DialogManagerEffect = () => {
const { dialogInternal } = useDialogManagerScopedStates();
const dialogInternal = useRecoilComponentValueV2(
dialogInternalComponentState,
);
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();

View File

@ -0,0 +1,3 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const DialogComponentInstanceContext = createComponentInstanceContext();

View File

@ -2,10 +2,11 @@ import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { useDialogManagerScopedStates } from '@/ui/feedback/dialog-manager/hooks/internal/useDialogManagerScopedStates';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
import { dialogInternalComponentState } from '@/ui/feedback/dialog-manager/states/dialogInternalComponentState';
import { DialogOptions } from '@/ui/feedback/dialog-manager/types/DialogOptions';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
const mockedUuid = 'mocked-uuid';
jest.mock('uuid');
@ -15,7 +16,7 @@ jest.mock('uuid');
const dialogManagerScopeId = 'dialog-manager';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
<DialogManagerScope dialogManagerScopeId={dialogManagerScopeId}>
<DialogManagerScope dialogComponentInstanceId={dialogManagerScopeId}>
{children}
</DialogManagerScope>
</RecoilRoot>
@ -74,12 +75,8 @@ const dialogOptionsArray: DialogOptionsArray = [
const renderHooks = () => {
const { result } = renderHook(
() => ({
dialogManager: useDialogManager({
dialogManagerScopeId: dialogManagerScopeId,
}),
internalState: useDialogManagerScopedStates({
dialogManagerScopeId,
}),
dialogManager: useDialogManager(),
dialogInternal: useRecoilComponentValueV2(dialogInternalComponentState),
}),
renderHookConfig,
);
@ -107,7 +104,7 @@ describe('useDialogManager', () => {
result.current.dialogManager.enqueueDialog(dialogOptionsArray[0]);
});
const { dialogInternal } = result.current.internalState;
const { dialogInternal } = result.current;
expect(dialogInternal.maxQueue).toEqual(2);
expect(dialogInternal.queue).toHaveLength(1);
@ -127,7 +124,7 @@ describe('useDialogManager', () => {
result.current.dialogManager.enqueueDialog(dialogOptionsArray[1]);
});
const { dialogInternal } = result.current.internalState;
const { dialogInternal } = result.current;
expect(dialogInternal.maxQueue).toEqual(2);
expect(dialogInternal.queue).toHaveLength(2);
@ -148,7 +145,7 @@ describe('useDialogManager', () => {
result.current.dialogManager.enqueueDialog(dialogOptionsArray[2]);
});
const { dialogInternal } = result.current.internalState;
const { dialogInternal } = result.current;
expect(dialogInternal.maxQueue).toEqual(2);
expect(dialogInternal.queue).toHaveLength(2);
@ -170,8 +167,7 @@ describe('useDialogManager', () => {
dialogOptionsArray[1],
]);
const { dialogInternal: stateAfterEnqueue } =
result.current.internalState;
const { dialogInternal: stateAfterEnqueue } = result.current;
expect(stateAfterEnqueue.maxQueue).toEqual(2);
expect(stateAfterEnqueue.queue).toHaveLength(2);
@ -186,7 +182,7 @@ describe('useDialogManager', () => {
queue: [],
};
const { dialogInternal: stateAfterClose } = result.current.internalState;
const { dialogInternal: stateAfterClose } = result.current;
expect(stateAfterClose).toEqual(expectReturnWhenClose);
expect(stateAfterClose.maxQueue).toEqual(2);

View File

@ -1,47 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { useDialogManagerScopedStates } from '@/ui/feedback/dialog-manager/hooks/internal/useDialogManagerScopedStates';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
const dialogManagerScopeId = 'dialog-manager';
const defaultReturnDialogState = { maxQueue: 2, queue: [] };
const updatedReturnDialogState = {
maxQueue: 5,
queue: [{ id: 'fakeId', title: 'testTitle', message: 'testMessage' }],
};
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<RecoilRoot>
<DialogManagerScope dialogManagerScopeId={dialogManagerScopeId}>
{children}
</DialogManagerScope>
</RecoilRoot>
);
};
describe('useDialogManagerScopedStates', () => {
it('Should return a dialog state and a function to update the state', async () => {
const { result } = renderHook(
() =>
useDialogManagerScopedStates({
dialogManagerScopeId,
}),
{
wrapper: Wrapper,
},
);
expect(result.current.dialogInternal).toEqual(defaultReturnDialogState);
expect(result.current.setDialogInternal).toBeInstanceOf(Function);
await act(async () => {
result.current.setDialogInternal(updatedReturnDialogState);
});
expect(result.current.dialogInternal).toEqual(updatedReturnDialogState);
});
});

View File

@ -1,25 +0,0 @@
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

@ -1,22 +1,17 @@
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { DIALOG_FOCUS_ID } from '@/ui/feedback/dialog-manager/constants/DialogFocusId';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { DialogManagerScopeInternalContext } from '../scopes/scope-internal-context/DialogManagerScopeInternalContext';
import { dialogInternalScopedState } from '../states/dialogInternalScopedState';
import { DialogComponentInstanceContext } from '@/ui/feedback/dialog-manager/contexts/DialogComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { dialogInternalComponentState } from '../states/dialogInternalComponentState';
import { DialogOptions } from '../types/DialogOptions';
type useDialogManagerProps = {
dialogManagerScopeId?: string;
};
export const useDialogManager = (props?: useDialogManagerProps) => {
const scopeId = useAvailableScopeIdOrThrow(
DialogManagerScopeInternalContext,
props?.dialogManagerScopeId,
export const useDialogManager = () => {
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
DialogComponentInstanceContext,
);
const { removeFocusItemFromFocusStackById } =
@ -25,33 +20,43 @@ export const useDialogManager = (props?: useDialogManagerProps) => {
const closeDialog = useRecoilCallback(
({ set }) =>
(id: string) => {
set(dialogInternalScopedState({ scopeId: scopeId }), (prevState) => ({
...prevState,
queue: prevState.queue.filter((dialog) => dialog.id !== id),
}));
set(
dialogInternalComponentState.atomFamily({
instanceId: componentInstanceId,
}),
(prevState) => ({
...prevState,
queue: prevState.queue.filter((dialog) => dialog.id !== id),
}),
);
removeFocusItemFromFocusStackById({ focusId: DIALOG_FOCUS_ID });
},
[removeFocusItemFromFocusStackById, scopeId],
[componentInstanceId, removeFocusItemFromFocusStackById],
);
const setDialogQueue = useRecoilCallback(
({ set }) =>
(newValue) =>
set(dialogInternalScopedState({ scopeId: scopeId }), (prev) => {
if (prev.queue.length >= prev.maxQueue) {
set(
dialogInternalComponentState.atomFamily({
instanceId: componentInstanceId,
}),
(prev) => {
if (prev.queue.length >= prev.maxQueue) {
return {
...prev,
queue: [...prev.queue.slice(1), newValue] as DialogOptions[],
};
}
return {
...prev,
queue: [...prev.queue.slice(1), newValue] as DialogOptions[],
queue: [...prev.queue, newValue] as DialogOptions[],
};
}
return {
...prev,
queue: [...prev.queue, newValue] as DialogOptions[],
};
}),
[scopeId],
},
),
[componentInstanceId],
);
const enqueueDialog = (options?: Omit<DialogOptions, 'id'>) => {

View File

@ -1,21 +1,21 @@
import { ReactNode } from 'react';
import { DialogManagerScopeInternalContext } from './scope-internal-context/DialogManagerScopeInternalContext';
import { DialogComponentInstanceContext } from '@/ui/feedback/dialog-manager/contexts/DialogComponentInstanceContext';
type DialogManagerScopeProps = {
children: ReactNode;
dialogManagerScopeId: string;
dialogComponentInstanceId: string;
};
export const DialogManagerScope = ({
children,
dialogManagerScopeId,
dialogComponentInstanceId,
}: DialogManagerScopeProps) => {
return (
<DialogManagerScopeInternalContext.Provider
value={{ scopeId: dialogManagerScopeId }}
<DialogComponentInstanceContext.Provider
value={{ instanceId: dialogComponentInstanceId }}
>
{children}
</DialogManagerScopeInternalContext.Provider>
</DialogComponentInstanceContext.Provider>
);
};

View File

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

View File

@ -0,0 +1,19 @@
import { DialogComponentInstanceContext } from '@/ui/feedback/dialog-manager/contexts/DialogComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { DialogOptions } from '../types/DialogOptions';
type DialogState = {
maxQueue: number;
queue: DialogOptions[];
};
export const dialogInternalComponentState = createComponentStateV2<DialogState>(
{
key: 'dialogInternalComponentState',
defaultValue: {
maxQueue: 2,
queue: [],
},
componentInstanceContext: DialogComponentInstanceContext,
},
);

View File

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