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:
@ -8,7 +8,7 @@ import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTa
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
@ -112,9 +112,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider cache={cache}>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
|
||||
@ -4,7 +4,7 @@ import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserve
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { AppRootErrorFallback } from '@/error-handler/components/AppRootErrorFallback';
|
||||
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
@ -25,7 +25,7 @@ export const App = () => {
|
||||
<I18nProvider i18n={i18n}>
|
||||
<RecoilDebugObserverEffect />
|
||||
<ApolloDevLogEffect />
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
<IconsProvider>
|
||||
<ExceptionHandlerProvider>
|
||||
<HelmetProvider>
|
||||
@ -37,7 +37,7 @@ export const App = () => {
|
||||
</HelmetProvider>
|
||||
</ExceptionHandlerProvider>
|
||||
</IconsProvider>
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</I18nProvider>
|
||||
</AppErrorBoundary>
|
||||
</RecoilRoot>
|
||||
|
||||
@ -2,7 +2,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
@ -14,8 +14,8 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { iconsState } from 'twenty-ui/display';
|
||||
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
|
||||
import { SupportDriver } from '~/generated/graphql';
|
||||
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
|
||||
|
||||
const redirectSpy = jest.fn();
|
||||
|
||||
@ -35,9 +35,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
|
||||
<RecoilRoot>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
|
||||
@ -4,7 +4,7 @@ import { ReactNode } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
import {
|
||||
query,
|
||||
@ -27,9 +27,9 @@ const mocks = [
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
@ -5,7 +5,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
import { MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
@ -55,9 +55,9 @@ const mocks = [
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
@ -14,7 +14,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
const mockCurrentUser = {
|
||||
id: 'fake-user-id',
|
||||
@ -45,9 +45,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider>
|
||||
<RecoilRoot initializeState={initializeState}>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
|
||||
@ -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: [],
|
||||
},
|
||||
});
|
||||
@ -13,7 +13,7 @@ import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||
import { ApolloCoreClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloCoreClientMockedProvider';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
|
||||
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
||||
import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
|
||||
@ -77,7 +77,7 @@ await dynamicActivate(SOURCE_LOCALE);
|
||||
const Providers = () => {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
<RecoilDebugObserverEffect />
|
||||
<ApolloProvider client={mockedApolloClient}>
|
||||
<I18nProvider i18n={i18n}>
|
||||
@ -125,7 +125,7 @@ const Providers = () => {
|
||||
</ClientConfigProvider>
|
||||
</I18nProvider>
|
||||
</ApolloProvider>
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Decorator } from '@storybook/react';
|
||||
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
export const SnackBarDecorator: Decorator = (Story) => (
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
<Story />
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
);
|
||||
|
||||
@ -6,7 +6,7 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
|
||||
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
|
||||
@ -25,7 +25,7 @@ export const getJestMetadataAndApolloMocksWrapper = ({
|
||||
}) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot initializeState={onInitializeRecoilSnapshot}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
<MockedProvider mocks={apolloMocks} addTypename={false} cache={cache}>
|
||||
<RecordFilterGroupsComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'instanceId' }}
|
||||
@ -53,7 +53,7 @@ export const getJestMetadataAndApolloMocksWrapper = ({
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
</RecordFilterGroupsComponentInstanceContext.Provider>
|
||||
</MockedProvider>
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { createApolloStoreFieldName } from '~/utils/createApolloStoreFieldName';
|
||||
|
||||
describe('createApolloStoreFieldName', () => {
|
||||
it('should create field name with simple variables', () => {
|
||||
const result = createApolloStoreFieldName({
|
||||
fieldName: 'users',
|
||||
fieldVariables: { limit: 10 },
|
||||
});
|
||||
|
||||
expect(result).toBe('users({"limit":10})');
|
||||
});
|
||||
|
||||
it('should create field name with complex variables', () => {
|
||||
const result = createApolloStoreFieldName({
|
||||
fieldName: 'companies',
|
||||
fieldVariables: {
|
||||
filter: { name: { ilike: '%test%' } },
|
||||
orderBy: [{ createdAt: 'DESC' }],
|
||||
first: 20,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
'companies({"filter":{"name":{"ilike":"%test%"}},"orderBy":[{"createdAt":"DESC"}],"first":20})',
|
||||
);
|
||||
});
|
||||
|
||||
it('should create field name with null and undefined values', () => {
|
||||
const result = createApolloStoreFieldName({
|
||||
fieldName: 'records',
|
||||
fieldVariables: {
|
||||
id: null,
|
||||
name: undefined,
|
||||
active: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('records({"id":null,"active":true})');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,188 @@
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
age: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
describe('sortByProperty', () => {
|
||||
const users: User[] = [
|
||||
{ id: 1, name: 'John', age: 30, score: 85.5 },
|
||||
{ id: 2, name: 'Alice', age: 25, score: 92.0 },
|
||||
{ id: 3, name: 'Bob', age: 35, score: 78.3 },
|
||||
{ id: 4, name: 'Charlie', age: 28, score: 88.7 },
|
||||
];
|
||||
|
||||
describe('string property sorting', () => {
|
||||
it('should sort by string property ascending', () => {
|
||||
const sorted = [...users].sort(sortByProperty('name', 'asc'));
|
||||
|
||||
expect(sorted.map((u) => u.name)).toEqual([
|
||||
'Alice',
|
||||
'Bob',
|
||||
'Charlie',
|
||||
'John',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort by string property descending', () => {
|
||||
const sorted = [...users].sort(sortByProperty('name', 'desc'));
|
||||
|
||||
expect(sorted.map((u) => u.name)).toEqual([
|
||||
'John',
|
||||
'Charlie',
|
||||
'Bob',
|
||||
'Alice',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should default to ascending when no direction specified', () => {
|
||||
const sorted = [...users].sort(sortByProperty('name'));
|
||||
|
||||
expect(sorted.map((u) => u.name)).toEqual([
|
||||
'Alice',
|
||||
'Bob',
|
||||
'Charlie',
|
||||
'John',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('number property sorting', () => {
|
||||
it('should sort by number property ascending', () => {
|
||||
const sorted = [...users].sort(sortByProperty('age', 'asc'));
|
||||
|
||||
expect(sorted.map((u) => u.age)).toEqual([25, 28, 30, 35]);
|
||||
});
|
||||
|
||||
it('should sort by number property descending', () => {
|
||||
const sorted = [...users].sort(sortByProperty('age', 'desc'));
|
||||
|
||||
expect(sorted.map((u) => u.age)).toEqual([35, 30, 28, 25]);
|
||||
});
|
||||
|
||||
it('should sort by decimal number property', () => {
|
||||
const sorted = [...users].sort(sortByProperty('score', 'asc'));
|
||||
|
||||
expect(sorted.map((u) => u.score)).toEqual([78.3, 85.5, 88.7, 92.0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty array', () => {
|
||||
const sorted: User[] = [].sort(sortByProperty('name', 'asc'));
|
||||
|
||||
expect(sorted).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single item array', () => {
|
||||
const singleUser = [{ name: 'John', age: 30 }];
|
||||
|
||||
const sorted = [...singleUser].sort(sortByProperty('name', 'asc'));
|
||||
|
||||
expect(sorted).toEqual(singleUser);
|
||||
});
|
||||
|
||||
it('should handle identical values', () => {
|
||||
const identicalUsers = [
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'John', age: 30 },
|
||||
];
|
||||
|
||||
const sorted = [...identicalUsers].sort(sortByProperty('age', 'asc'));
|
||||
|
||||
expect(sorted).toEqual(identicalUsers);
|
||||
});
|
||||
|
||||
it('should handle mixed case strings', () => {
|
||||
const mixedCaseUsers = [
|
||||
{ name: 'john' },
|
||||
{ name: 'ALICE' },
|
||||
{ name: 'Bob' },
|
||||
{ name: 'charlie' },
|
||||
];
|
||||
|
||||
const sorted = [...mixedCaseUsers].sort(sortByProperty('name', 'asc'));
|
||||
|
||||
expect(sorted.map((u) => u.name)).toEqual([
|
||||
'ALICE',
|
||||
'Bob',
|
||||
'charlie',
|
||||
'john',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle negative numbers', () => {
|
||||
const numbersArray = [
|
||||
{ value: -10 },
|
||||
{ value: 5 },
|
||||
{ value: -3 },
|
||||
{ value: 0 },
|
||||
{ value: 15 },
|
||||
];
|
||||
|
||||
const sorted = [...numbersArray].sort(sortByProperty('value', 'asc'));
|
||||
|
||||
expect(sorted.map((n) => n.value)).toEqual([-10, -3, 0, 5, 15]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error for unsupported property type', () => {
|
||||
const objectsWithUnsupportedType = [
|
||||
{ data: { nested: 'value' } },
|
||||
{ data: { nested: 'other' } },
|
||||
];
|
||||
|
||||
expect(() => {
|
||||
[...objectsWithUnsupportedType].sort(sortByProperty('data', 'asc'));
|
||||
}).toThrow(
|
||||
'Property type not supported in sortByProperty, only string and number are supported',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for array property type', () => {
|
||||
const objectsWithArrays = [{ tags: ['a', 'b'] }, { tags: ['c', 'd'] }];
|
||||
|
||||
expect(() => {
|
||||
[...objectsWithArrays].sort(sortByProperty('tags', 'asc'));
|
||||
}).toThrow(
|
||||
'Property type not supported in sortByProperty, only string and number are supported',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for boolean property type', () => {
|
||||
const objectsWithBoolean = [{ active: true }, { active: false }];
|
||||
|
||||
expect(() => {
|
||||
[...objectsWithBoolean].sort(sortByProperty('active', 'asc'));
|
||||
}).toThrow(
|
||||
'Property type not supported in sortByProperty, only string and number are supported',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety', () => {
|
||||
it('should work with different object shapes', () => {
|
||||
const products = [
|
||||
{ name: 'Laptop', price: 999.99, category: 'Electronics' },
|
||||
{ name: 'Book', price: 19.99, category: 'Education' },
|
||||
{ name: 'Chair', price: 149.5, category: 'Furniture' },
|
||||
];
|
||||
|
||||
const sortedByName = [...products].sort(sortByProperty('name', 'asc'));
|
||||
|
||||
expect(sortedByName.map((p) => p.name)).toEqual([
|
||||
'Book',
|
||||
'Chair',
|
||||
'Laptop',
|
||||
]);
|
||||
|
||||
const sortedByPrice = [...products].sort(sortByProperty('price', 'desc'));
|
||||
expect(sortedByPrice.map((p) => p.price)).toEqual([999.99, 149.5, 19.99]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,124 @@
|
||||
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
|
||||
|
||||
describe('getFileNameAndExtension', () => {
|
||||
it('should split filename with extension correctly', () => {
|
||||
const result = getFileNameAndExtension('document.pdf');
|
||||
expect(result).toEqual({
|
||||
name: 'document',
|
||||
extension: '.pdf',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with multiple dots', () => {
|
||||
const result = getFileNameAndExtension('my.file.name.txt');
|
||||
expect(result).toEqual({
|
||||
name: 'my.file.name',
|
||||
extension: '.txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with no extension', () => {
|
||||
const result = getFileNameAndExtension('README');
|
||||
expect(result).toEqual({
|
||||
name: '',
|
||||
extension: 'README',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files starting with dot', () => {
|
||||
const result = getFileNameAndExtension('.gitignore');
|
||||
expect(result).toEqual({
|
||||
name: '',
|
||||
extension: '.gitignore',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle hidden files with extension', () => {
|
||||
const result = getFileNameAndExtension('.env.local');
|
||||
expect(result).toEqual({
|
||||
name: '.env',
|
||||
extension: '.local',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = getFileNameAndExtension('');
|
||||
expect(result).toEqual({
|
||||
name: '',
|
||||
extension: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with long extensions', () => {
|
||||
const result = getFileNameAndExtension('archive.tar.gz');
|
||||
expect(result).toEqual({
|
||||
name: 'archive.tar',
|
||||
extension: '.gz',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with special characters', () => {
|
||||
const result = getFileNameAndExtension('file-name_with@special#chars.xlsx');
|
||||
expect(result).toEqual({
|
||||
name: 'file-name_with@special#chars',
|
||||
extension: '.xlsx',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with spaces', () => {
|
||||
const result = getFileNameAndExtension('my document file.docx');
|
||||
expect(result).toEqual({
|
||||
name: 'my document file',
|
||||
extension: '.docx',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with path separators in name', () => {
|
||||
const result = getFileNameAndExtension('folder/subfolder/file.txt');
|
||||
expect(result).toEqual({
|
||||
name: 'folder/subfolder/file',
|
||||
extension: '.txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with only extension', () => {
|
||||
const result = getFileNameAndExtension('.txt');
|
||||
expect(result).toEqual({
|
||||
name: '',
|
||||
extension: '.txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with multiple consecutive dots', () => {
|
||||
const result = getFileNameAndExtension('file...txt');
|
||||
expect(result).toEqual({
|
||||
name: 'file..',
|
||||
extension: '.txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle very long filenames', () => {
|
||||
const longName = 'a'.repeat(100);
|
||||
const result = getFileNameAndExtension(`${longName}.pdf`);
|
||||
expect(result).toEqual({
|
||||
name: longName,
|
||||
extension: '.pdf',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle files with numbers', () => {
|
||||
const result = getFileNameAndExtension('report-2023-12-01.csv');
|
||||
expect(result).toEqual({
|
||||
name: 'report-2023-12-01',
|
||||
extension: '.csv',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle uppercase extensions', () => {
|
||||
const result = getFileNameAndExtension('DOCUMENT.PDF');
|
||||
expect(result).toEqual({
|
||||
name: 'DOCUMENT',
|
||||
extension: '.PDF',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,80 @@
|
||||
import { splitFullName } from '~/utils/format/spiltFullName';
|
||||
|
||||
describe('splitFullName', () => {
|
||||
it('should split a full name with two parts', () => {
|
||||
const result = splitFullName('John Doe');
|
||||
expect(result).toEqual(['John', 'Doe']);
|
||||
});
|
||||
|
||||
it('should handle names with extra whitespace', () => {
|
||||
const result = splitFullName(' John Doe ');
|
||||
expect(result).toEqual(['John', 'Doe']);
|
||||
});
|
||||
|
||||
it('should handle single name', () => {
|
||||
const result = splitFullName('John');
|
||||
expect(result).toEqual(['John', '']);
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
const result = splitFullName('');
|
||||
expect(result).toEqual(['', '']);
|
||||
});
|
||||
|
||||
it('should handle only whitespace', () => {
|
||||
const result = splitFullName(' ');
|
||||
expect(result).toEqual(['', '']);
|
||||
});
|
||||
|
||||
it('should handle names with more than two parts', () => {
|
||||
const result = splitFullName('John Michael Doe');
|
||||
expect(result).toEqual(['John', 'Michael']);
|
||||
});
|
||||
|
||||
it('should handle names with multiple middle names', () => {
|
||||
const result = splitFullName('John Michael Christopher Doe');
|
||||
expect(result).toEqual(['John', 'Michael']);
|
||||
});
|
||||
|
||||
it('should handle names with hyphenated parts', () => {
|
||||
const result = splitFullName('Mary-Jane Watson-Smith');
|
||||
expect(result).toEqual(['Mary-Jane', 'Watson-Smith']);
|
||||
});
|
||||
|
||||
it('should handle names with apostrophes', () => {
|
||||
const result = splitFullName("John O'Connor");
|
||||
expect(result).toEqual(['John', "O'Connor"]);
|
||||
});
|
||||
|
||||
it('should handle names with special characters', () => {
|
||||
const result = splitFullName('José García-López');
|
||||
expect(result).toEqual(['José', 'García-López']);
|
||||
});
|
||||
|
||||
it('should handle names with multiple consecutive spaces', () => {
|
||||
const result = splitFullName('John Doe');
|
||||
expect(result).toEqual(['John', 'Doe']);
|
||||
});
|
||||
|
||||
it('should handle names with tabs and newlines', () => {
|
||||
const result = splitFullName('John\t\nDoe');
|
||||
expect(result).toEqual(['John', 'Doe']);
|
||||
});
|
||||
|
||||
it('should handle very long names', () => {
|
||||
const result = splitFullName(
|
||||
'John Michael Christopher Alexander Doe Smith Johnson',
|
||||
);
|
||||
expect(result).toEqual(['John', 'Michael']);
|
||||
});
|
||||
|
||||
it('should handle single character names', () => {
|
||||
const result = splitFullName('J D');
|
||||
expect(result).toEqual(['J', 'D']);
|
||||
});
|
||||
|
||||
it('should handle names with numbers', () => {
|
||||
const result = splitFullName('John Doe II');
|
||||
expect(result).toEqual(['John', 'Doe']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user