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']); + }); +});