Refactor snackbar old component scoped state (#13183)

This PR refactors the snackbar modules that was using legacy versions of
our state management.

We replace the old states with new ones and also the old scoped context
with component instance context.
This commit is contained in:
Lucas Bordeau
2025-07-11 18:05:09 +02:00
committed by GitHub
parent 69a6f4471e
commit bba1b296c1
23 changed files with 554 additions and 178 deletions
@@ -1,13 +1,14 @@
import styled from '@emotion/styled';
import { AnimatePresence, motion } from 'framer-motion';
import { useSnackBarManagerScopedStates } from '@/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { snackBarInternalComponentState } from '@/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { SnackBar } from './SnackBar';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { SnackBar } from './SnackBar';
const StyledSnackBarContainer = styled.div`
bottom: ${({ theme }) => theme.spacing(3)};
@@ -26,7 +27,10 @@ const StyledSnackBarContainer = styled.div`
`;
export const SnackBarProvider = ({ children }: React.PropsWithChildren) => {
const { snackBarInternal } = useSnackBarManagerScopedStates();
const snackBarInternal = useRecoilComponentValueV2(
snackBarInternalComponentState,
);
const { handleSnackBarClose } = useSnackBar();
const isMobile = useIsMobile();
@@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const SnackBarComponentInstanceContext =
createComponentInstanceContext();
@@ -1,45 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { useSnackBarManagerScopedStates } from '@/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { SnackBarState } from '@/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState';
const snackBarManagerScopeId = 'snack-bar-manager';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<RecoilRoot>
<SnackBarProviderScope snackBarManagerScopeId={snackBarManagerScopeId}>
{children}
</SnackBarProviderScope>
</RecoilRoot>
);
};
describe('useSnackBarManagerScopedStates', () => {
it('should return snackbar state and a function to update the state', async () => {
const { result } = renderHook(
() =>
useSnackBarManagerScopedStates({
snackBarManagerScopeId,
}),
{ wrapper: Wrapper },
);
const defaultState = { maxQueue: 3, queue: [] };
expect(result.current.snackBarInternal).toEqual(defaultState);
const newSnackBarState: SnackBarState = {
maxQueue: 5,
queue: [{ id: 'testid', role: 'alert', message: 'TEST MESSAGE' }],
};
act(() => {
result.current.setSnackBarInternal(newSnackBarState);
});
expect(result.current.snackBarInternal).toEqual(newSnackBarState);
});
});
@@ -1,24 +0,0 @@
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
import { snackBarInternalScopedState } from '@/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState';
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useSnackBarManagerScopedStatesProps = {
snackBarManagerScopeId?: string;
};
export const useSnackBarManagerScopedStates = (
props?: useSnackBarManagerScopedStatesProps,
) => {
const scopeId = useAvailableScopeIdOrThrow(
SnackBarManagerScopeInternalContext,
props?.snackBarManagerScopeId,
);
const [snackBarInternal, setSnackBarInternal] = useRecoilScopedStateV2(
snackBarInternalScopedState,
scopeId,
);
return { snackBarInternal, setSnackBarInternal };
};
@@ -3,59 +3,69 @@ import { useRecoilCallback } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import {
snackBarInternalScopedState,
snackBarInternalComponentState,
SnackBarOptions,
} from '@/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
} from '@/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util';
export const useSnackBar = () => {
const scopeId = useAvailableScopeIdOrThrow(
SnackBarManagerScopeInternalContext,
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
SnackBarComponentInstanceContext,
);
const handleSnackBarClose = useRecoilCallback(
({ set }) =>
(id: string) => {
set(snackBarInternalScopedState({ scopeId }), (prevState) => ({
...prevState,
queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
}));
set(
snackBarInternalComponentState.atomFamily({
instanceId: componentInstanceId,
}),
(prevState) => ({
...prevState,
queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
}),
);
},
[scopeId],
[componentInstanceId],
);
const setSnackBarQueue = useRecoilCallback(
({ set }) =>
(newValue: SnackBarOptions) =>
set(snackBarInternalScopedState({ scopeId }), (prev) => {
if (
isDefined(newValue.dedupeKey) &&
prev.queue.some(
(snackBar) => snackBar.dedupeKey === newValue.dedupeKey,
)
) {
return prev;
}
set(
snackBarInternalComponentState.atomFamily({
instanceId: componentInstanceId,
}),
(prev) => {
if (
isDefined(newValue.dedupeKey) &&
prev.queue.some(
(snackBar) => snackBar.dedupeKey === newValue.dedupeKey,
)
) {
return prev;
}
if (prev.queue.length >= prev.maxQueue) {
return {
...prev,
queue: [...prev.queue.slice(1), newValue] as SnackBarOptions[],
};
}
if (prev.queue.length >= prev.maxQueue) {
return {
...prev,
queue: [...prev.queue.slice(1), newValue] as SnackBarOptions[],
queue: [...prev.queue, newValue] as SnackBarOptions[],
};
}
return {
...prev,
queue: [...prev.queue, newValue] as SnackBarOptions[],
};
}),
[scopeId],
},
),
[componentInstanceId],
);
const enqueueSuccessSnackBar = useCallback(
@@ -0,0 +1,21 @@
import { ReactNode } from 'react';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
type SnackBarComponentInstanceContextProviderProps = {
children: ReactNode;
snackBarComponentInstanceId: string;
};
export const SnackBarComponentInstanceContextProvider = ({
children,
snackBarComponentInstanceId,
}: SnackBarComponentInstanceContextProviderProps) => {
return (
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: snackBarComponentInstanceId }}
>
{children}
</SnackBarComponentInstanceContext.Provider>
);
};
@@ -1,21 +0,0 @@
import { ReactNode } from 'react';
import { SnackBarManagerScopeInternalContext } from './scope-internal-context/SnackBarManagerScopeInternalContext';
type SnackBarProviderScopeProps = {
children: ReactNode;
snackBarManagerScopeId: string;
};
export const SnackBarProviderScope = ({
children,
snackBarManagerScopeId,
}: SnackBarProviderScopeProps) => {
return (
<SnackBarManagerScopeInternalContext.Provider
value={{ scopeId: snackBarManagerScopeId }}
>
{children}
</SnackBarManagerScopeInternalContext.Provider>
);
};
@@ -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 SnackBarManagerScopeInternalContextProps = RecoilComponentStateKey;
export const SnackBarManagerScopeInternalContext =
createScopeInternalContext<SnackBarManagerScopeInternalContextProps>();
@@ -0,0 +1,22 @@
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { SnackBarProps } from '../components/SnackBar';
export type SnackBarOptions = SnackBarProps & {
id: string;
};
export type SnackBarState = {
maxQueue: number;
queue: SnackBarOptions[];
};
export const snackBarInternalComponentState =
createComponentStateV2<SnackBarState>({
key: 'snackBarState',
defaultValue: {
maxQueue: 3,
queue: [],
},
componentInstanceContext: SnackBarComponentInstanceContext,
});
@@ -1,20 +0,0 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { SnackBarProps } from '../components/SnackBar';
export type SnackBarOptions = SnackBarProps & {
id: string;
};
export type SnackBarState = {
maxQueue: number;
queue: SnackBarOptions[];
};
export const snackBarInternalScopedState = createComponentState<SnackBarState>({
key: 'snackBarState',
defaultValue: {
maxQueue: 3,
queue: [],
},
});