From bba1b296c1df7dcf0aff62f57cb83d00178c1307 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 11 Jul 2025 18:05:09 +0200 Subject: [PATCH] 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. --- .../useActivityTargetObjectRecords.test.tsx | 6 +- .../src/modules/app/components/App.tsx | 6 +- .../auth/hooks/__tests__/useAuth.test.tsx | 8 +- .../useFindManyObjectMetadataItems.test.tsx | 6 +- .../useFilteredSearchRecordQuery.test.tsx | 6 +- .../useSettingsNavigationItems.test.tsx | 6 +- .../components/SnackBarProvider.tsx | 10 +- .../SnackBarComponentInstanceContext.ts | 4 + .../useSnackBarManagerScopedStates.test.tsx | 45 ----- .../useSnackBarManagerScopedStates.ts | 24 --- .../snack-bar-manager/hooks/useSnackBar.ts | 70 ++++--- ...ackBarComponentInstanceContextProvider.tsx | 21 ++ .../scopes/SnackBarProviderScope.tsx | 21 -- .../SnackBarManagerScopeInternalContext.ts | 7 - .../states/snackBarInternalComponentState.ts | 22 ++ .../states/snackBarInternalScopedState.ts | 20 -- .../src/testing/decorators/PageDecorator.tsx | 6 +- .../testing/decorators/SnackBarDecorator.tsx | 6 +- .../getJestMetadataAndApolloMocksWrapper.tsx | 6 +- .../createApolloStoreFieldName.test.ts | 40 ++++ .../array/__tests__/sortByProperty.test.ts | 188 ++++++++++++++++++ .../__tests__/getFileNameAndExtension.test.ts | 124 ++++++++++++ .../format/__tests__/spiltFullName.test.ts | 80 ++++++++ 23 files changed, 554 insertions(+), 178 deletions(-) create mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext.ts delete mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/__tests__/useSnackBarManagerScopedStates.test.tsx delete mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates.ts create mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider.tsx delete mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope.tsx delete mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext.ts create mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState.ts delete mode 100644 packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState.ts create mode 100644 packages/twenty-front/src/utils/__tests__/createApolloStoreFieldName.test.ts create mode 100644 packages/twenty-front/src/utils/array/__tests__/sortByProperty.test.ts create mode 100644 packages/twenty-front/src/utils/file/__tests__/getFileNameAndExtension.test.ts create mode 100644 packages/twenty-front/src/utils/format/__tests__/spiltFullName.test.ts diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx index ba8f488ca..8fd953eb6 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx @@ -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 }) => ( - + {children} - + diff --git a/packages/twenty-front/src/modules/app/components/App.tsx b/packages/twenty-front/src/modules/app/components/App.tsx index a1a564924..cfccd1fa5 100644 --- a/packages/twenty-front/src/modules/app/components/App.tsx +++ b/packages/twenty-front/src/modules/app/components/App.tsx @@ -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 = () => { - + @@ -37,7 +37,7 @@ export const App = () => { - + diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index 4d05f8364..1184a5de2 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -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 }) => ( - + {children} - + diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFindManyObjectMetadataItems.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFindManyObjectMetadataItems.test.tsx index 70b4051d7..e9ece3207 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFindManyObjectMetadataItems.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFindManyObjectMetadataItems.test.tsx @@ -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 }) => ( - + {children} - + ); diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx index ae02a841f..c87dbfb51 100644 --- a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx +++ b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx @@ -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 }) => ( - + {children} - + ); diff --git a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx index b80cd5a21..7bade12cc 100644 --- a/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/__tests__/useSettingsNavigationItems.test.tsx @@ -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 }) => ( - + {children} - + diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx index 8c549a976..4213c4a0e 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBarProvider.tsx @@ -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(); diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext.ts new file mode 100644 index 000000000..9105eea8e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const SnackBarComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/__tests__/useSnackBarManagerScopedStates.test.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/__tests__/useSnackBarManagerScopedStates.test.tsx deleted file mode 100644 index c67a6632f..000000000 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/__tests__/useSnackBarManagerScopedStates.test.tsx +++ /dev/null @@ -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 ( - - - {children} - - - ); -}; - -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); - }); -}); diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates.ts deleted file mode 100644 index ae58104cc..000000000 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates.ts +++ /dev/null @@ -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 }; -}; diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts index 1f2d62d33..5ffd097dc 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts @@ -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( diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider.tsx new file mode 100644 index 000000000..6b0917598 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope.tsx deleted file mode 100644 index caffabe2b..000000000 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope.tsx +++ /dev/null @@ -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 ( - - {children} - - ); -}; diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext.ts deleted file mode 100644 index c9686c2d2..000000000 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext.ts +++ /dev/null @@ -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(); diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState.ts new file mode 100644 index 000000000..6b1d6602f --- /dev/null +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState.ts @@ -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({ + key: 'snackBarState', + defaultValue: { + maxQueue: 3, + queue: [], + }, + componentInstanceContext: SnackBarComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState.ts deleted file mode 100644 index a880abccc..000000000 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState.ts +++ /dev/null @@ -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({ - key: 'snackBarState', - defaultValue: { - maxQueue: 3, - queue: [], - }, -}); diff --git a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx index ed3ed18e2..7ae2dbeed 100644 --- a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx @@ -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 ( - + @@ -125,7 +125,7 @@ const Providers = () => { - + ); }; diff --git a/packages/twenty-front/src/testing/decorators/SnackBarDecorator.tsx b/packages/twenty-front/src/testing/decorators/SnackBarDecorator.tsx index be3320cb4..6e89147dc 100644 --- a/packages/twenty-front/src/testing/decorators/SnackBarDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/SnackBarDecorator.tsx @@ -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) => ( - + - + ); diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx index 211676f19..94520d5c1 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx @@ -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 }) => ( - + - + ); }; diff --git a/packages/twenty-front/src/utils/__tests__/createApolloStoreFieldName.test.ts b/packages/twenty-front/src/utils/__tests__/createApolloStoreFieldName.test.ts new file mode 100644 index 000000000..b1a486b16 --- /dev/null +++ b/packages/twenty-front/src/utils/__tests__/createApolloStoreFieldName.test.ts @@ -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})'); + }); +}); diff --git a/packages/twenty-front/src/utils/array/__tests__/sortByProperty.test.ts b/packages/twenty-front/src/utils/array/__tests__/sortByProperty.test.ts new file mode 100644 index 000000000..2f8bd7f50 --- /dev/null +++ b/packages/twenty-front/src/utils/array/__tests__/sortByProperty.test.ts @@ -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]); + }); + }); +}); diff --git a/packages/twenty-front/src/utils/file/__tests__/getFileNameAndExtension.test.ts b/packages/twenty-front/src/utils/file/__tests__/getFileNameAndExtension.test.ts new file mode 100644 index 000000000..9a0032cb9 --- /dev/null +++ b/packages/twenty-front/src/utils/file/__tests__/getFileNameAndExtension.test.ts @@ -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', + }); + }); +}); diff --git a/packages/twenty-front/src/utils/format/__tests__/spiltFullName.test.ts b/packages/twenty-front/src/utils/format/__tests__/spiltFullName.test.ts new file mode 100644 index 000000000..3bfcaa8e7 --- /dev/null +++ b/packages/twenty-front/src/utils/format/__tests__/spiltFullName.test.ts @@ -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']); + }); +});