diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/__tests__/useRecordBoard.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/__tests__/useRecordBoard.test.tsx
new file mode 100644
index 000000000..3ab88feef
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/__tests__/useRecordBoard.test.tsx
@@ -0,0 +1,88 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
+import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+const recordBoardScopeId = 'recordBoardScopeId';
+
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+const useRecordBoardHook = () => {
+ const recordBoard = useRecordBoard({ recordBoardScopeId });
+ const { isBoardLoadedState, boardColumnsState, onFieldsChangeState } =
+ useRecordBoardScopedStates({
+ recordBoardScopeId: recordBoardScopeId,
+ });
+ const isBoardLoaded = useRecoilValue(isBoardLoadedState);
+ const boardColumns = useRecoilValue(boardColumnsState);
+ const onFieldsChange = useRecoilValue(onFieldsChangeState);
+
+ return {
+ recordBoard,
+ isBoardLoaded,
+ boardColumns,
+ onFieldsChange,
+ };
+};
+
+describe('useRecordBoard', () => {
+ it('should set isBoardLoadedState', async () => {
+ const { result } = renderHook(() => useRecordBoardHook(), renderHookConfig);
+
+ act(() => {
+ result.current.recordBoard.setIsBoardLoaded(true);
+ });
+
+ await waitFor(() => {
+ expect(result.current.isBoardLoaded).toBe(true);
+ });
+ });
+
+ it('should set boardColumnsState', async () => {
+ const columns = [
+ {
+ id: '1',
+ title: '1',
+ position: 1,
+ },
+ {
+ id: '1',
+ title: '1',
+ position: 1,
+ },
+ ];
+ const { result } = renderHook(() => useRecordBoardHook(), renderHookConfig);
+
+ act(() => {
+ result.current.recordBoard.setBoardColumns(columns);
+ });
+
+ await waitFor(() => {
+ expect(result.current.boardColumns).toEqual(columns);
+ });
+ });
+
+ it('should set setOnFieldsChange', async () => {
+ const onFieldsChangeFunction = () => {};
+ const onFieldsChange = jest.fn(() => onFieldsChangeFunction);
+ const { result } = renderHook(() => useRecordBoardHook(), renderHookConfig);
+
+ act(() => {
+ result.current.recordBoard.setOnFieldsChange(onFieldsChange);
+ });
+
+ await waitFor(() => {
+ expect(result.current.onFieldsChange).toEqual(onFieldsChangeFunction);
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useCreateOpportunity.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useCreateOpportunity.test.tsx
new file mode 100644
index 000000000..b136e44f1
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useCreateOpportunity.test.tsx
@@ -0,0 +1,78 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { act, renderHook } from '@testing-library/react';
+import gql from 'graphql-tag';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { useCreateOpportunity } from '@/object-record/record-board/hooks/internal/useCreateOpportunity';
+import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board/states/recordBoardCardIdsByColumnIdFamilyState';
+
+const mockedUuid = 'mocked-uuid';
+jest.mock('uuid', () => ({
+ v4: () => mockedUuid,
+}));
+
+jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
+ useMapFieldMetadataToGraphQLQuery: () => () => '\n',
+}));
+
+const mocks = [
+ {
+ request: {
+ query: gql`
+ mutation CreateOneOpportunity($input: OpportunityCreateInput!) {
+ createOpportunity(data: $input) {
+ id
+ }
+ }
+ `,
+ variables: {
+ input: {
+ id: mockedUuid,
+ pipelineStepId: 'pipelineStepId',
+ companyId: 'New Opportunity',
+ },
+ },
+ },
+ result: jest.fn(() => ({
+ data: { createOpportunity: { id: '' } },
+ })),
+ },
+];
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+describe('useCreateOpportunity', () => {
+ it('should create opportunity successfully', () => {
+ const companyIdname = 'New Opportunity';
+ const opportunityPipelineStepId = 'pipelineStepId';
+
+ const { result } = renderHook(
+ () => ({
+ createOpportunity: useCreateOpportunity(),
+ recordBoardCardIdsByColumnId: useRecoilValue(
+ recordBoardCardIdsByColumnIdFamilyState(opportunityPipelineStepId),
+ ),
+ }),
+ renderHookConfig,
+ );
+
+ act(() => {
+ result.current.createOpportunity(
+ companyIdname,
+ opportunityPipelineStepId,
+ );
+ });
+
+ expect(result.current.recordBoardCardIdsByColumnId).toStrictEqual([
+ mockedUuid,
+ ]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useCurrentRecordBoardCardSelectedInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useCurrentRecordBoardCardSelectedInternal.test.tsx
new file mode 100644
index 000000000..be0655577
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useCurrentRecordBoardCardSelectedInternal.test.tsx
@@ -0,0 +1,56 @@
+import { act, renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext';
+import { useCurrentRecordBoardCardSelectedInternal } from '@/object-record/record-board/hooks/internal/useCurrentRecordBoardCardSelectedInternal';
+import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
+import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
+import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
+
+const scopeId = 'scopeId';
+const boardCardId = 'boardCardId';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+);
+
+describe('useCurrentRecordBoardCardSelectedInternal', () => {
+ it('should update the data when selecting and deselecting the cardId', () => {
+ const { result } = renderHook(
+ () => ({
+ currentCardSelect: useCurrentRecordBoardCardSelectedInternal(),
+ activeCardIdsState: useRecoilValue(
+ useRecordBoardScopedStates().activeCardIdsState,
+ ),
+ actionBarOpenState: useRecoilValue(actionBarOpenState),
+ }),
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ expect(result.current.activeCardIdsState).toStrictEqual([]);
+ expect(result.current.actionBarOpenState).toBe(false);
+ expect(result.current.currentCardSelect.isCurrentCardSelected).toBe(false);
+
+ act(() => {
+ result.current.currentCardSelect.setCurrentCardSelected(true);
+ });
+
+ expect(result.current.activeCardIdsState).toStrictEqual([boardCardId]);
+ expect(result.current.actionBarOpenState).toBe(true);
+ expect(result.current.currentCardSelect.isCurrentCardSelected).toBe(true);
+
+ act(() => {
+ result.current.currentCardSelect.setCurrentCardSelected(false);
+ });
+
+ expect(result.current.activeCardIdsState).toStrictEqual([]);
+ expect(result.current.actionBarOpenState).toBe(false);
+ expect(result.current.currentCardSelect.isCurrentCardSelected).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useDeleteSelectedRecordBoardCardsInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useDeleteSelectedRecordBoardCardsInternal.test.tsx
new file mode 100644
index 000000000..cc8114ada
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useDeleteSelectedRecordBoardCardsInternal.test.tsx
@@ -0,0 +1,123 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { act, renderHook, waitFor } from '@testing-library/react';
+import gql from 'graphql-tag';
+import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
+
+import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext';
+import { useCreateOpportunity } from '@/object-record/record-board/hooks/internal/useCreateOpportunity';
+import { useCurrentRecordBoardCardSelectedInternal } from '@/object-record/record-board/hooks/internal/useCurrentRecordBoardCardSelectedInternal';
+import { useDeleteSelectedRecordBoardCardsInternal } from '@/object-record/record-board/hooks/internal/useDeleteSelectedRecordBoardCardsInternal';
+import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
+import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
+import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board/states/recordBoardCardIdsByColumnIdFamilyState';
+
+jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
+ useMapFieldMetadataToGraphQLQuery: jest.fn().mockReturnValue(() => '\n'),
+}));
+
+const mockedUuid = 'mocked-uuid';
+jest.mock('uuid', () => ({ v4: () => mockedUuid }));
+
+const mocks = [
+ {
+ request: {
+ query: gql`
+ mutation DeleteManyOpportunities($filter: OpportunityFilterInput!) {
+ deleteOpportunities(filter: $filter) {
+ id
+ }
+ }
+ `,
+ variables: { filter: { id: { in: [mockedUuid] } } },
+ },
+ result: jest.fn(() => ({
+ data: { deleteOpportunities: { id: '' } },
+ })),
+ },
+ {
+ request: {
+ query: gql`
+ mutation CreateOneOpportunity($input: OpportunityCreateInput!) {
+ createOpportunity(data: $input) {
+ id
+ }
+ }
+ `,
+ variables: {
+ input: {
+ id: mockedUuid,
+ pipelineStepId: 'pipelineStepId',
+ companyId: 'New Opportunity',
+ },
+ },
+ },
+ result: jest.fn(() => ({
+ data: { createOpportunity: { id: '' } },
+ })),
+ },
+];
+
+const scopeId = 'scopeId';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ {children}
+
+
+
+);
+
+describe('useDeleteSelectedRecordBoardCardsInternal', () => {
+ it('should run apollo mutation and update recoil state when delete selected cards', async () => {
+ const companyIdname = 'New Opportunity';
+ const opportunityPipelineStepId = 'pipelineStepId';
+
+ const { result } = renderHook(
+ () => ({
+ createOpportunity: useCreateOpportunity(),
+ deleteSelectedCards: useDeleteSelectedRecordBoardCardsInternal(),
+ setBoardColumns: useSetRecoilState(
+ useRecordBoardScopedStates({
+ recordBoardScopeId: scopeId,
+ }).boardColumnsState,
+ ),
+ recordBoardCardIdsByColumnId: useRecoilValue(
+ recordBoardCardIdsByColumnIdFamilyState(opportunityPipelineStepId),
+ ),
+ currentSelect: useCurrentRecordBoardCardSelectedInternal(),
+ }),
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ act(() => {
+ result.current.currentSelect.setCurrentCardSelected(true);
+ result.current.setBoardColumns([
+ {
+ id: opportunityPipelineStepId,
+ title: '1',
+ position: 1,
+ },
+ ]);
+ result.current.createOpportunity(
+ companyIdname,
+ opportunityPipelineStepId,
+ );
+ });
+
+ expect(result.current.recordBoardCardIdsByColumnId).toStrictEqual([
+ mockedUuid,
+ ]);
+ await act(async () => {
+ await result.current.deleteSelectedCards();
+ });
+
+ await waitFor(() => {
+ expect(result.current.recordBoardCardIdsByColumnId).toStrictEqual([]);
+ expect(mocks[0].result).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardActionBarEntriesInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardActionBarEntriesInternal.test.tsx
new file mode 100644
index 000000000..8608e91bc
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardActionBarEntriesInternal.test.tsx
@@ -0,0 +1,54 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { useDeleteSelectedRecordBoardCardsInternal } from '@/object-record/record-board/hooks/internal/useDeleteSelectedRecordBoardCardsInternal';
+import { useRecordBoardActionBarEntriesInternal } from '@/object-record/record-board/hooks/internal/useRecordBoardActionBarEntriesInternal';
+import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
+import { IconTrash } from '@/ui/display/icon';
+import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
+
+const scopeId = 'scopeId';
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+);
+
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+describe('useRecordBoardActionBarEntriesInternal', () => {
+ it('should update actionBarEntries', async () => {
+ const { result } = renderHook(() => {
+ const deleteSelectedBoardCards =
+ useDeleteSelectedRecordBoardCardsInternal();
+ const newActionBarEntry = {
+ label: 'Delete',
+ Icon: IconTrash,
+ accent: 'danger',
+ onClick: deleteSelectedBoardCards,
+ };
+ return {
+ setActionBarEntries: useRecordBoardActionBarEntriesInternal(),
+ actionBarEntries: useRecoilValue(actionBarEntriesState),
+ newActionBarEntry,
+ };
+ }, renderHookConfig);
+
+ expect(result.current.actionBarEntries).toStrictEqual([]);
+
+ act(() => {
+ result.current.setActionBarEntries.setActionBarEntries();
+ });
+
+ await waitFor(() => {
+ expect(JSON.stringify(result.current.actionBarEntries)).toBe(
+ JSON.stringify([result.current.newActionBarEntry]),
+ );
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardCardFieldsInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardCardFieldsInternal.test.tsx
new file mode 100644
index 000000000..3a7b5645e
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardCardFieldsInternal.test.tsx
@@ -0,0 +1,110 @@
+import { act } from 'react-dom/test-utils';
+import { renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
+
+import { FieldType } from '@/object-record/field/types/FieldType';
+import { useRecordBoardCardFieldsInternal } from '@/object-record/record-board/hooks/internal/useRecordBoardCardFieldsInternal';
+import { onFieldsChangeScopedState } from '@/object-record/record-board/states/onFieldsChangeScopedState';
+import { recordBoardCardFieldsScopedState } from '@/object-record/record-board/states/recordBoardCardFieldsScopedState';
+import { savedRecordBoardCardFieldsScopedState } from '@/object-record/record-board/states/savedRecordBoardCardFieldsScopedState';
+
+const recordBoardScopeId = 'recordBoardScopeId';
+
+const renderHookConfig = {
+ wrapper: RecoilRoot,
+};
+
+describe('useRecordBoardCardFieldsInternal', () => {
+ it('should toggle field visibility', async () => {
+ const { result } = renderHook(() => {
+ const [cardFieldsList, setCardFieldsList] = useRecoilState(
+ recordBoardCardFieldsScopedState({ scopeId: recordBoardScopeId }),
+ );
+ return {
+ boardCardFields: useRecordBoardCardFieldsInternal({
+ recordBoardScopeId,
+ }),
+ cardFieldsList,
+ setCardFieldsList,
+ };
+ }, renderHookConfig);
+
+ const field = {
+ position: 0,
+ fieldMetadataId: 'id',
+ label: 'label',
+ iconName: 'icon',
+ type: 'TEXT' as FieldType,
+ metadata: {
+ fieldName: 'fieldName',
+ },
+ isVisible: true,
+ };
+
+ act(() => {
+ result.current.setCardFieldsList([field]);
+ });
+
+ expect(result.current.cardFieldsList[0].isVisible).toBe(true);
+
+ act(() => {
+ result.current.boardCardFields.handleFieldVisibilityChange(field);
+ });
+
+ expect(result.current.cardFieldsList[0].isVisible).toBe(false);
+
+ act(() => {
+ result.current.boardCardFields.handleFieldVisibilityChange({
+ ...field,
+ isVisible: false,
+ });
+ });
+
+ expect(result.current.cardFieldsList[0].isVisible).toBe(true);
+ });
+
+ it('should call the onFieldsChange callback and update board card states', async () => {
+ const { result } = renderHook(() => {
+ const [onFieldsChange, setOnFieldsChange] = useRecoilState(
+ onFieldsChangeScopedState({ scopeId: recordBoardScopeId }),
+ );
+ return {
+ boardCardFieldsHook: useRecordBoardCardFieldsInternal({
+ recordBoardScopeId,
+ }),
+ boardCardFieldsList: useRecoilValue(
+ recordBoardCardFieldsScopedState({ scopeId: recordBoardScopeId }),
+ ),
+ savedBoardCardFieldsList: useRecoilValue(
+ savedRecordBoardCardFieldsScopedState({
+ scopeId: recordBoardScopeId,
+ }),
+ ),
+ onFieldsChange,
+ setOnFieldsChange,
+ };
+ }, renderHookConfig);
+
+ const field = {
+ position: 0,
+ fieldMetadataId: 'id',
+ label: 'label',
+ iconName: 'icon',
+ type: 'TEXT' as FieldType,
+ metadata: {
+ fieldName: 'fieldName',
+ },
+ isVisible: true,
+ };
+ const onChangeFunction = jest.fn();
+
+ await act(async () => {
+ result.current.setOnFieldsChange(() => onChangeFunction);
+ result.current.boardCardFieldsHook.handleFieldsReorder([field]);
+ });
+
+ expect(onChangeFunction).toHaveBeenCalledWith([field]);
+ expect(result.current.savedBoardCardFieldsList).toStrictEqual([field]);
+ expect(result.current.boardCardFieldsList).toStrictEqual([field]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardColumnsInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardColumnsInternal.test.tsx
new file mode 100644
index 000000000..21f8a974c
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardColumnsInternal.test.tsx
@@ -0,0 +1,128 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { act, renderHook, waitFor } from '@testing-library/react';
+import gql from 'graphql-tag';
+import { RecoilRoot, useRecoilState, useSetRecoilState } from 'recoil';
+
+import { useBoardColumnsInternal } from '@/object-record/record-board/hooks/internal/useRecordBoardColumnsInternal';
+import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
+import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
+import { BoardColumnDefinition } from '@/object-record/record-board/types/BoardColumnDefinition';
+
+jest.mock('@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery', () => ({
+ useMapFieldMetadataToGraphQLQuery: jest.fn().mockReturnValue(() => '\n'),
+}));
+
+const mocks = [
+ {
+ request: {
+ query: gql`
+ mutation UpdateOnePipelineStep(
+ $idToUpdate: ID!
+ $input: PipelineStepUpdateInput!
+ ) {
+ updatePipelineStep(id: $idToUpdate, data: $input) {
+ id
+ }
+ }
+ `,
+ variables: { idToUpdate: '1', input: { position: 0 } },
+ },
+ result: jest.fn(() => ({
+ data: { updatePipelineStep: { id: '' } },
+ })),
+ },
+];
+
+const scopeId = 'scopeId';
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+);
+
+const renderHookConfig = {
+ wrapper: Wrapper,
+};
+
+describe('useBoardColumnsInternal', () => {
+ it('should update boardColumns state when moving to left and right', async () => {
+ const { result } = renderHook(() => {
+ const [boardColumnsList, setBoardColumnsList] = useRecoilState(
+ useRecordBoardScopedStates().boardColumnsState,
+ );
+ return {
+ boardColumns: useBoardColumnsInternal(),
+ boardColumnsList,
+ setBoardColumnsList,
+ };
+ }, renderHookConfig);
+ const columns: BoardColumnDefinition[] = [
+ {
+ id: '1',
+ title: '1',
+ position: 0,
+ },
+ {
+ id: '2',
+ title: '2',
+ position: 1,
+ },
+ ];
+ act(() => {
+ result.current.setBoardColumnsList(columns);
+ });
+
+ act(() => {
+ result.current.boardColumns.handleMoveBoardColumn('right', columns[0]);
+ });
+
+ await waitFor(() => {
+ expect(result.current.boardColumnsList).toStrictEqual([
+ { id: '2', title: '2', position: 0, index: 0 },
+ { id: '1', title: '1', position: 1, index: 1 },
+ ]);
+ });
+
+ act(() => {
+ result.current.boardColumns.handleMoveBoardColumn('left', columns[0]);
+ });
+
+ await waitFor(() => {
+ expect(result.current.boardColumnsList).toStrictEqual([
+ { id: '1', title: '1', position: 0, index: 0 },
+ { id: '2', title: '2', position: 1, index: 1 },
+ ]);
+ });
+ });
+
+ it('should call apollo mutation after persistBoardColumns', async () => {
+ const { result } = renderHook(() => {
+ return {
+ boardColumns: useBoardColumnsInternal(),
+ setBoardColumnsList: useSetRecoilState(
+ useRecordBoardScopedStates().boardColumnsState,
+ ),
+ };
+ }, renderHookConfig);
+ const columns: BoardColumnDefinition[] = [
+ {
+ id: '1',
+ title: '1',
+ position: 0,
+ },
+ ];
+ act(() => {
+ result.current.setBoardColumnsList(columns);
+ });
+
+ act(() => {
+ result.current.boardColumns.persistBoardColumns();
+ });
+
+ await waitFor(() => {
+ expect(mocks[0].result).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardContextMenuEntriesInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardContextMenuEntriesInternal.test.tsx
new file mode 100644
index 000000000..44e4bb9bb
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useRecordBoardContextMenuEntriesInternal.test.tsx
@@ -0,0 +1,55 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { useDeleteSelectedRecordBoardCardsInternal } from '@/object-record/record-board/hooks/internal/useDeleteSelectedRecordBoardCardsInternal';
+import { useRecordBoardContextMenuEntriesInternal } from '@/object-record/record-board/hooks/internal/useRecordBoardContextMenuEntriesInternal';
+import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
+import { IconTrash } from '@/ui/display/icon';
+import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
+
+const scopeId = 'scopeId';
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+);
+
+describe('useRecordBoardContextMenuEntriesInternal', () => {
+ it('should update contextEntries', async () => {
+ const { result } = renderHook(
+ () => {
+ const deleteSelectedBoardCards =
+ useDeleteSelectedRecordBoardCardsInternal();
+ const newContextEntry = {
+ label: 'Delete',
+ Icon: IconTrash,
+ accent: 'danger',
+ onClick: deleteSelectedBoardCards,
+ };
+ return {
+ setContextEntries: useRecordBoardContextMenuEntriesInternal(),
+ contextEntries: useRecoilValue(contextMenuEntriesState),
+ newContextEntry,
+ };
+ },
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ expect(result.current.contextEntries).toStrictEqual([]);
+
+ act(() => {
+ result.current.setContextEntries.setContextMenuEntries();
+ });
+
+ await waitFor(() => {
+ expect(JSON.stringify(result.current.contextEntries)).toBe(
+ JSON.stringify([result.current.newContextEntry]),
+ );
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useSetRecordBoardCardSelectedInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useSetRecordBoardCardSelectedInternal.test.tsx
new file mode 100644
index 000000000..d2f65b74d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useSetRecordBoardCardSelectedInternal.test.tsx
@@ -0,0 +1,58 @@
+import { act, renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { useSetRecordBoardCardSelectedInternal } from '@/object-record/record-board/hooks/internal/useSetRecordBoardCardSelectedInternal';
+import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
+import { isRecordBoardCardSelectedFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedFamilyState';
+
+const scopeId = 'scopeId';
+const boardCardId = 'boardCardId';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+const recordBoardScopeId = 'recordBoardScopeId';
+
+describe('useSetRecordBoardCardSelectedInternal', () => {
+ it('should update the data when selecting and deselecting the cardId', async () => {
+ const { result } = renderHook(
+ () => {
+ return {
+ cardSelect: useSetRecordBoardCardSelectedInternal({
+ recordBoardScopeId,
+ }),
+ isSelected: useRecoilValue(
+ isRecordBoardCardSelectedFamilyState(boardCardId),
+ ),
+ };
+ },
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ expect(result.current.isSelected).toBe(false);
+
+ act(() => {
+ result.current.cardSelect.setCardSelected(boardCardId, true);
+ });
+
+ expect(result.current.isSelected).toBe(true);
+
+ act(() => {
+ result.current.cardSelect.setCardSelected(boardCardId, false);
+ });
+
+ expect(result.current.isSelected).toBe(false);
+
+ act(() => {
+ result.current.cardSelect.setCardSelected(boardCardId, true);
+ result.current.cardSelect.unselectAllActiveCards();
+ });
+
+ expect(result.current.isSelected).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useUpdateCompanyBoardColumnsInternal.test.tsx b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useUpdateCompanyBoardColumnsInternal.test.tsx
new file mode 100644
index 000000000..fe794f193
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/__tests__/useUpdateCompanyBoardColumnsInternal.test.tsx
@@ -0,0 +1,113 @@
+import { act, renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { CompanyForBoard } from '@/companies/types/CompanyProgress';
+import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
+import { useUpdateCompanyBoardColumnsInternal } from '@/object-record/record-board/hooks/internal/useUpdateCompanyBoardColumnsInternal';
+import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
+import { recordBoardCardIdsByColumnIdFamilyState } from '@/object-record/record-board/states/recordBoardCardIdsByColumnIdFamilyState';
+import { currentPipelineStepsState } from '@/pipeline/states/currentPipelineStepsState';
+import { Opportunity } from '@/pipeline/types/Opportunity';
+import { PipelineStep } from '@/pipeline/types/PipelineStep';
+
+const scopeId = 'scopeId';
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+describe('useUpdateCompanyBoardColumnsInternal', () => {
+ it('should update recoil state after updateCompanyBoardColumns call ', async () => {
+ const { result } = renderHook(
+ () => {
+ return {
+ updateCompanyBoardColumns: useUpdateCompanyBoardColumnsInternal(),
+ currentPipeline: useRecoilValue(currentPipelineStepsState),
+ boardColumns: useRecoilValue(
+ useRecordBoardScopedStates().boardColumnsState,
+ ),
+ savedBoardColumns: useRecoilValue(
+ useRecordBoardScopedStates().savedBoardColumnsState,
+ ),
+ idsByColumnId: useRecoilValue(
+ recordBoardCardIdsByColumnIdFamilyState('1'),
+ ),
+ };
+ },
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ const pipelineSteps: PipelineStep[] = [
+ {
+ id: '1',
+ name: 'Step 1',
+ color: 'red',
+ position: 1,
+ createdAt: '2024-01-12',
+ updatedAt: '2024-01-12',
+ },
+ {
+ id: '2',
+ name: 'Step 2',
+ color: 'blue',
+ position: 1,
+ createdAt: '2024-01-12',
+ updatedAt: '2024-01-12',
+ },
+ ];
+ const opportunity: Opportunity = {
+ id: '123',
+ amount: {
+ amountMicros: 1000000,
+ currencyCode: 'USD',
+ },
+ closeDate: new Date('2024-02-01'),
+ probability: 0.75,
+ pipelineStepId: '1',
+ pipelineStep: pipelineSteps[0],
+ pointOfContactId: '456',
+ pointOfContact: {
+ id: '456',
+ name: {
+ firstName: 'John',
+ lastName: 'Doe',
+ },
+ avatarUrl: 'https://example.com/avatar.jpg',
+ },
+ };
+
+ const companyForBoard: CompanyForBoard = {
+ id: '789',
+ name: 'Acme Inc.',
+ domainName: 'acme.com',
+ };
+
+ expect(result.current.currentPipeline).toStrictEqual([]);
+ expect(result.current.savedBoardColumns).toStrictEqual([]);
+ expect(result.current.boardColumns).toStrictEqual([]);
+ expect(result.current.idsByColumnId).toStrictEqual([]);
+
+ act(() => {
+ result.current.updateCompanyBoardColumns(
+ pipelineSteps,
+ [opportunity],
+ [companyForBoard],
+ );
+ });
+
+ const expectedBoardColumns = [
+ { id: '1', title: 'Step 1', colorCode: 'red', position: 1 },
+ { id: '2', title: 'Step 2', colorCode: 'blue', position: 1 },
+ ];
+
+ expect(result.current.currentPipeline).toStrictEqual(pipelineSteps);
+ expect(result.current.savedBoardColumns).toStrictEqual(
+ expectedBoardColumns,
+ );
+ expect(result.current.boardColumns).toStrictEqual(expectedBoardColumns);
+ expect(result.current.idsByColumnId).toStrictEqual([opportunity.id]);
+ });
+});