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