From fd2b063d8443a95d1771b67723d38ef634be8343 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 14 Aug 2025 15:00:07 +0200 Subject: [PATCH] Add RecordField abstraction (#13923) This PR adds a new RecordField abstraction to mimick the behavior of RecordFilter, RecordSort and other similar concepts that allow to have something that is not related directly to views. It is closely following the same pattern with an Effect component to initialize it from views and CRUD hooks to modify it. Although for this concept we save to view fields automatically for every modification. This PR does not implement saving to view fields, it is just a parallel code path that is meant to be built to see if everything behaves the same then we'll remove the old code path at the end by saving record fields instead of the many states for columns, fields definitions, etc. --- packages/twenty-front/jest.config.ts | 6 +- .../components/CommandMenuContainer.tsx | 46 ++++------ .../__stories__/CommandMenu.stories.tsx | 38 +++----- .../components/CommandMenuMergeRecordPage.tsx | 42 ++++----- .../components/CommandMenuRecordPage.tsx | 72 +++++++-------- ...RecordComponentInstanceContextsWrapper.tsx | 34 +++++++ .../ObjectOptionsDropdownContent.stories.tsx | 38 +++----- .../hooks/useObjectOptionsForTable.ts | 10 ++- .../record-field/hooks/useMoveRecordField.ts | 70 +++++++++++++++ .../hooks/useReorderRecordFields.ts | 39 ++++++++ .../hooks/useUpdateRecordField.ts | 58 ++++++++++++ .../hooks/useUpsertRecordField.ts | 53 +++++++++++ .../RecordFieldsComponentInstanceContext.ts | 4 + .../currentRecordFieldsComponentState.ts | 11 +++ .../record-field/types/RecordField.ts | 10 +++ .../components/RecordIndexContainerGater.tsx | 46 ++++------ .../hooks/useHandleColumnsChange.ts | 1 + .../record-table/hooks/useTableColumns.ts | 29 ++++++ .../components/RecordTableHeaderCell.tsx | 37 +++++--- .../SignInBackgroundMockContainer.tsx | 70 +++++++-------- .../src/modules/views/components/ViewBar.tsx | 2 + .../components/ViewBarRecordFieldEffect.tsx | 65 ++++++++++++++ .../ViewBarFilterDropdown.stories.tsx | 42 ++++----- ...CurrentRecordFieldsComponentFamilyState.ts | 9 ++ .../views/utils/areRecordFieldsEqual.ts | 22 +++++ .../views/utils/mapViewFieldToRecordField.ts | 15 ++++ .../pages/object-record/RecordShowPage.tsx | 78 +++++++--------- .../src/testing/decorators/PageDecorator.tsx | 27 ++---- .../decorators/RecordTableDecorator.tsx | 42 ++++----- ...dataAndApolloMocksAndActionMenuWrapper.tsx | 88 ++++++++----------- .../getJestMetadataAndApolloMocksWrapper.tsx | 36 +++----- 31 files changed, 717 insertions(+), 423 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/states/context/RecordFieldsComponentInstanceContext.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/states/currentRecordFieldsComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/types/RecordField.ts create mode 100644 packages/twenty-front/src/modules/views/components/ViewBarRecordFieldEffect.tsx create mode 100644 packages/twenty-front/src/modules/views/states/hasInitializedCurrentRecordFieldsComponentFamilyState.ts create mode 100644 packages/twenty-front/src/modules/views/utils/areRecordFieldsEqual.ts create mode 100644 packages/twenty-front/src/modules/views/utils/mapViewFieldToRecordField.ts diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index 95a088439b..c4e8c7ac94 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -58,9 +58,9 @@ const jestConfig: JestConfigWithTsJest = { extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { global: { - statements: 55, - lines: 54, - functions: 44, + statements: 54, + lines: 53, + functions: 43, }, }, collectCoverageFrom: ['/src/**/*.ts'], diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx index 429239aae0..b9ecb32537 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx @@ -8,9 +8,7 @@ import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -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 { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { AnimatePresence } from 'framer-motion'; @@ -50,35 +48,23 @@ export const CommandMenuContainer = ({ useCommandMenuHotKeys(); return ( - - + - - - - - {isCommandMenuOpened && ( - - {children} - - )} - - - - - - + {isCommandMenuOpened && ( + {children} + )} + + + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 95d615234a..d9239fbb78 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -25,9 +25,7 @@ import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpe import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType'; -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 { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { HttpResponse, graphql } from 'msw'; import { IconDotsVertical } from 'twenty-ui/display'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; @@ -38,33 +36,25 @@ const openTimeout = 50; const ContextStoreDecorator: Decorator = (Story) => { return ( - - - - - - - - - - - - - + + + + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx index 1edad710e6..3c9ebef8fc 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuMergeRecordPage.tsx @@ -3,10 +3,8 @@ import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/c import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { MergeRecordsContainer } from '@/object-record/record-merge/components/MergeRecordsContainer'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext'; import styled from '@emotion/styled'; @@ -35,31 +33,23 @@ export const CommandMenuMergeRecordPage = () => { } return ( - - - - - - - - - - - - - + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuRecordPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuRecordPage.tsx index 16b6d9a707..fdf4cb5002 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuRecordPage.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/record-page/components/CommandMenuRecordPage.tsx @@ -5,12 +5,10 @@ import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/r import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { INFORMATION_BANNER_HEIGHT } from '@/information-banner/constants/InformationBannerHeight'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect'; import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext'; @@ -71,48 +69,40 @@ export const CommandMenuRecordPage = () => { } return ( - - - - - - - - - - - - - - - - + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx b/packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx new file mode 100644 index 0000000000..8843190a0b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx @@ -0,0 +1,34 @@ +import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext'; +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 { type PropsWithChildren } from 'react'; + +export type RecordComponentInstanceContextsWrapperProps = PropsWithChildren<{ + componentInstanceId: string; +}>; + +export const RecordComponentInstanceContextsWrapper = ({ + componentInstanceId, + children, +}: RecordComponentInstanceContextsWrapperProps) => { + return ( + + + + + {children} + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx index 6a9e06255a..7e8a013c54 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx @@ -1,14 +1,12 @@ import { type Meta, type StoryObj } from '@storybook/react'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent'; import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId'; import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; import { type ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; @@ -42,30 +40,20 @@ const meta: Meta = { }, [setObjectMetadataItems]); return ( - - - - + + - - - - - - - - - + + + + + ); }, ContextStoreDecorator, diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts index ba3f919cca..a24a8e2540 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForTable.ts @@ -1,6 +1,7 @@ import { type OnDragEndResponder } from '@hello-pangea/dnd'; import { useCallback } from 'react'; +import { useReorderRecordFields } from '@/object-record/record-field/hooks/useReorderRecordFields'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; @@ -24,6 +25,8 @@ export const useObjectOptionsForTable = ( { recordTableId, objectMetadataId }, ); + const { reorderRecordFields } = useReorderRecordFields(); + const handleReorderColumns: OnDragEndResponder = useCallback( async (result) => { if ( @@ -39,9 +42,14 @@ export const useObjectOptionsForTable = ( toIndex: result.destination.index - 1, }); + reorderRecordFields({ + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }); + handleColumnReorder(reorderedFields); }, - [visibleTableColumns, handleColumnReorder], + [visibleTableColumns, handleColumnReorder, reorderRecordFields], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts new file mode 100644 index 0000000000..c72742c4d0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useMoveRecordField.ts @@ -0,0 +1,70 @@ +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilCallback } from 'recoil'; + +export const useMoveRecordField = () => { + const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( + currentRecordFieldsComponentState, + ); + + const moveRecordField = useRecoilCallback( + ({ set, snapshot }) => + ({ + direction, + fieldMetadataItemIdToMove, + }: { + direction: 'before' | 'after'; + fieldMetadataItemIdToMove: string; + }) => { + const currentRecordFields = snapshot + .getLoadable(currentRecordFieldsCallbackState) + .getValue(); + + const indexOfRecordFieldToMove = currentRecordFields.findIndex( + (recordField) => + recordField.fieldMetadataItemId === fieldMetadataItemIdToMove, + ); + + if (indexOfRecordFieldToMove === -1) { + return; + } + + const newRecordFields = [...currentRecordFields]; + + const targetArrayIndex = + direction === 'before' + ? indexOfRecordFieldToMove - 1 + : indexOfRecordFieldToMove + 1; + + const targetArraySize = newRecordFields.length - 1; + + if ( + indexOfRecordFieldToMove >= 0 && + targetArrayIndex >= 0 && + indexOfRecordFieldToMove <= targetArraySize && + targetArrayIndex <= targetArraySize + ) { + const currentRecordField = newRecordFields[indexOfRecordFieldToMove]; + const targetRecordField = newRecordFields[targetArrayIndex]; + + const targetRecordFieldNewPosition = currentRecordField.position; + const currentRecordFieldNewPosition = targetRecordField.position; + + newRecordFields[indexOfRecordFieldToMove] = { + ...newRecordFields[indexOfRecordFieldToMove], + position: currentRecordFieldNewPosition, + }; + + newRecordFields[targetRecordFieldNewPosition] = { + ...newRecordFields[targetRecordFieldNewPosition], + position: targetRecordFieldNewPosition, + }; + + set(currentRecordFieldsCallbackState, newRecordFields); + } + }, + [currentRecordFieldsCallbackState], + ); + + return { moveRecordField }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts new file mode 100644 index 0000000000..c71ca91edc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderRecordFields.ts @@ -0,0 +1,39 @@ +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilCallback } from 'recoil'; +import { moveArrayItem } from '~/utils/array/moveArrayItem'; + +export const useReorderRecordFields = () => { + const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( + currentRecordFieldsComponentState, + ); + + const reorderRecordFields = useRecoilCallback( + ({ set, snapshot }) => + ({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => { + const currentRecordFields = snapshot + .getLoadable(currentRecordFieldsCallbackState) + .getValue(); + + const reorderedRecordFields = moveArrayItem(currentRecordFields, { + fromIndex, + toIndex, + }); + + const reorderedRecordFieldsWithNewPosition = + reorderedRecordFields.map((recordField, index) => ({ + ...recordField, + position: index, + })); + + set( + currentRecordFieldsCallbackState, + reorderedRecordFieldsWithNewPosition, + ); + }, + [currentRecordFieldsCallbackState], + ); + + return { reorderRecordFields }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts new file mode 100644 index 0000000000..a252426c8d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpdateRecordField.ts @@ -0,0 +1,58 @@ +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; + +export const useUpdateRecordField = () => { + const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( + currentRecordFieldsComponentState, + ); + + const updateRecordField = useRecoilCallback( + ({ set, snapshot }) => + ( + fieldMetadataItemId: string, + partialRecordField: Partial< + Pick + >, + ) => { + const currentRecordFields = getSnapshotValue( + snapshot, + currentRecordFieldsCallbackState, + ); + + const foundRecordFieldInCurrentRecordFields = currentRecordFields.some( + (existingRecordField) => + existingRecordField.fieldMetadataItemId === fieldMetadataItemId, + ); + + if (!foundRecordFieldInCurrentRecordFields) { + throw new Error( + `Cannot find record field to update with field metadata item id : ${fieldMetadataItemId}`, + ); + } else { + set(currentRecordFieldsCallbackState, (currentRecordFields) => { + const newCurrentRecordFields = [...currentRecordFields]; + + const indexOfRecordFieldToUpdate = newCurrentRecordFields.findIndex( + (existingRecordField) => + existingRecordField.fieldMetadataItemId === fieldMetadataItemId, + ); + + newCurrentRecordFields[indexOfRecordFieldToUpdate] = { + ...newCurrentRecordFields[indexOfRecordFieldToUpdate], + ...partialRecordField, + }; + + return newCurrentRecordFields; + }); + } + }, + [currentRecordFieldsCallbackState], + ); + + return { + updateRecordField, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts new file mode 100644 index 0000000000..71fe1b2e07 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useUpsertRecordField.ts @@ -0,0 +1,53 @@ +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; + +export const useUpsertRecordField = () => { + const currentRecordFieldsCallbackState = useRecoilComponentCallbackState( + currentRecordFieldsComponentState, + ); + + const upsertRecordField = useRecoilCallback( + ({ set, snapshot }) => + (recordFieldToUpsert: RecordField) => { + const currentRecordFields = getSnapshotValue( + snapshot, + currentRecordFieldsCallbackState, + ); + + const foundRecordFieldInCurrentRecordFields = currentRecordFields.some( + (existingRecordField) => + existingRecordField.id === recordFieldToUpsert.id, + ); + + if (!foundRecordFieldInCurrentRecordFields) { + set(currentRecordFieldsCallbackState, [ + ...currentRecordFields, + recordFieldToUpsert, + ]); + } else { + set(currentRecordFieldsCallbackState, (currentRecordFields) => { + const newCurrentRecordFields = [...currentRecordFields]; + + const indexOfRecordFieldToUpdate = newCurrentRecordFields.findIndex( + (existingRecordField) => + existingRecordField.id === recordFieldToUpsert.id, + ); + + newCurrentRecordFields[indexOfRecordFieldToUpdate] = { + ...recordFieldToUpsert, + }; + + return newCurrentRecordFields; + }); + } + }, + [currentRecordFieldsCallbackState], + ); + + return { + upsertRecordField, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/context/RecordFieldsComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-field/states/context/RecordFieldsComponentInstanceContext.ts new file mode 100644 index 0000000000..4260a24689 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/context/RecordFieldsComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const RecordFieldsComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/currentRecordFieldsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/currentRecordFieldsComponentState.ts new file mode 100644 index 0000000000..8c4d539863 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/currentRecordFieldsComponentState.ts @@ -0,0 +1,11 @@ +import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext'; +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const currentRecordFieldsComponentState = createComponentState< + RecordField[] +>({ + key: 'currentRecordFieldsComponentState', + defaultValue: [], + componentInstanceContext: RecordFieldsComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/RecordField.ts b/packages/twenty-front/src/modules/object-record/record-field/types/RecordField.ts new file mode 100644 index 0000000000..55abe5c69e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/RecordField.ts @@ -0,0 +1,10 @@ +import { type AggregateOperations } from '~/generated/graphql'; + +export type RecordField = { + id: string; + fieldMetadataItemId: string; + position: number; + isVisible: boolean; + size: number; + aggregateOperation?: AggregateOperations | null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx index 8f11426e4a..daf0a761b0 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx @@ -3,17 +3,15 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer'; import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect'; import { RecordIndexLoadBaseOnContextStoreEffect } from '@/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect'; import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader'; import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { PageBody } from '@/ui/layout/page/components/PageBody'; import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass'; import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; @@ -74,34 +72,26 @@ export const RecordIndexContainerGater = () => { - - - - + + + - - - - - - - - - - - - + + + + + + diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts index 165fd1e13f..fc4b006eb9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useHandleColumnsChange.ts @@ -5,6 +5,7 @@ import { type ColumnDefinition } from '@/object-record/record-table/types/Column // TODO: see how we can better abstract this and set the correct interaction between view and table // but for now it allows to have a cleaner API globally. +// TODO: should be solved with new RecordField abstraction export const useHandleColumnsChange = () => { const { saveColumnsToView } = useSaveColumnsToView(); const { setTableColumns } = useSetTableColumns(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts index 9b52807324..4d10fd617e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts @@ -4,11 +4,15 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldM import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell'; import { useMoveViewColumns } from '@/views/hooks/useMoveViewColumns'; +import { useMoveRecordField } from '@/object-record/record-field/hooks/useMoveRecordField'; +import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; +import { useUpsertRecordField } from '@/object-record/record-field/hooks/useUpsertRecordField'; import { useHandleColumnsChange } from '@/object-record/record-table/hooks/useHandleColumnsChange'; import { availableTableColumnsComponentState } from '@/object-record/record-table/states/availableTableColumnsComponentState'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { v4 } from 'uuid'; import { type ColumnDefinition } from '../types/ColumnDefinition'; type useRecordTableProps = { @@ -40,6 +44,9 @@ export const useTableColumns = ({ const { handleColumnsChange } = useHandleColumnsChange(); + const { updateRecordField } = useUpdateRecordField(); + const { upsertRecordField } = useUpsertRecordField(); + const handleColumnVisibilityChange = useCallback( async ( viewField: Omit, 'size' | 'position'>, @@ -68,6 +75,14 @@ export const useTableColumns = ({ { ...newColumn, isVisible: true, position: lastPosition + 1 }, ]; + upsertRecordField({ + id: v4(), + fieldMetadataItemId: viewField.fieldMetadataId, + size: 100, + isVisible: true, + position: lastPosition + 1, + }); + await handleColumnsChange({ columns: nextColumns, objectMetadataId, @@ -80,6 +95,10 @@ export const useTableColumns = ({ : previousColumn, ); + updateRecordField(viewField.fieldMetadataId, { + isVisible: !viewField.isVisible, + }); + await handleColumnsChange({ columns: nextColumns, objectMetadataId, @@ -88,15 +107,19 @@ export const useTableColumns = ({ } }, [ + upsertRecordField, tableColumns, availableTableColumns, handleColumnsChange, visibleTableColumns, objectMetadataId, recordTableId, + updateRecordField, ], ); + const { moveRecordField } = useMoveRecordField(); + const handleMoveTableColumn = useCallback( async ( direction: 'left' | 'right', @@ -115,6 +138,11 @@ export const useTableColumns = ({ visibleTableColumns, ); + moveRecordField({ + direction: direction === 'left' ? 'before' : 'after', + fieldMetadataItemIdToMove: column.fieldMetadataId, + }); + await handleColumnsChange({ columns, objectMetadataId, @@ -128,6 +156,7 @@ export const useTableColumns = ({ handleColumnsChange, objectMetadataId, recordTableId, + moveRecordField, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 3577cca20c..846d83de6a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import { useCallback, useMemo, useState } from 'react'; import { useRecoilCallback } from 'recoil'; +import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly'; import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; @@ -135,13 +136,17 @@ export const RecordTableHeaderCell = ({ const [initialPointerPositionX, setInitialPointerPositionX] = useState< number | null >(null); - const [resizedFieldKey, setResizedFieldKey] = useState(null); + const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = useState< + string | null + >(null); const { handleColumnsChange } = useTableColumns({ objectMetadataId: objectMetadataItem.id, recordTableId, }); + const { updateRecordField } = useUpdateRecordField(); + const handleResizeHandlerStart = useCallback( ({ x }) => { setInitialPointerPositionX(x); @@ -162,7 +167,7 @@ export const RecordTableHeaderCell = ({ const handleResizeHandlerEnd = useRecoilCallback( ({ snapshot, set }) => async () => { - if (!resizedFieldKey) return; + if (!resizedFieldMetadataItemId) return; const resizeFieldOffset = getSnapshotValue( snapshot, @@ -171,22 +176,27 @@ export const RecordTableHeaderCell = ({ const nextWidth = Math.round( Math.max( - tableColumnsByKey[resizedFieldKey].size + resizeFieldOffset, + tableColumnsByKey[resizedFieldMetadataItemId].size + + resizeFieldOffset, COLUMN_MIN_WIDTH, ), ); set(resizeFieldOffsetState, 0); setInitialPointerPositionX(null); - setResizedFieldKey(null); + setResizedFieldMetadataItemId(null); - if (nextWidth !== tableColumnsByKey[resizedFieldKey].size) { + if (nextWidth !== tableColumnsByKey[resizedFieldMetadataItemId].size) { const nextColumns = tableColumns.map((column) => - column.fieldMetadataId === resizedFieldKey + column.fieldMetadataId === resizedFieldMetadataItemId ? { ...column, size: nextWidth } : column, ); + updateRecordField(resizedFieldMetadataItemId, { + size: nextWidth, + }); + await handleColumnsChange({ columns: nextColumns, objectMetadataId: objectMetadataItem.id, @@ -195,19 +205,20 @@ export const RecordTableHeaderCell = ({ } }, [ - resizedFieldKey, + resizedFieldMetadataItemId, resizeFieldOffsetState, tableColumnsByKey, - setResizedFieldKey, + setResizedFieldMetadataItemId, tableColumns, handleColumnsChange, objectMetadataItem, recordTableId, + updateRecordField, ], ); useTrackPointer({ - shouldTrackPointer: resizedFieldKey !== null, + shouldTrackPointer: resizedFieldMetadataItemId !== null, onMouseDown: handleResizeHandlerStart, onMouseMove: handleResizeHandlerMove, onMouseUp: handleResizeHandlerEnd, @@ -251,10 +262,12 @@ export const RecordTableHeaderCell = ({ return ( { - setResizedFieldKey(column.fieldMetadataId); + setResizedFieldMetadataItemId(column.fieldMetadataId); }} /> )} diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx index fa6e7a07c4..3114e5dd07 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx @@ -5,10 +5,8 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -57,48 +55,40 @@ export const SignInBackgroundMockContainer = () => { - - - + - - - - {isDefined(objectMetadataItem) && ( - <> - } - /> + {isDefined(objectMetadataItem) && ( + <> + } + /> - {}} - /> - - )} - - - - - + {}} + /> + + )} + + + diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx index b64744d706..f6ba1af7f8 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx @@ -14,6 +14,7 @@ import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/cons import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext'; import { ViewBarAnyFieldFilterEffect } from '@/views/components/ViewBarAnyFieldFilterEffect'; import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown'; +import { ViewBarRecordFieldEffect } from '@/views/components/ViewBarRecordFieldEffect'; import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect'; import { ViewBarRecordFilterGroupEffect } from '@/views/components/ViewBarRecordFilterGroupEffect'; import { ViewBarRecordSortEffect } from '@/views/components/ViewBarRecordSortEffect'; @@ -45,6 +46,7 @@ export const ViewBar = ({ > + diff --git a/packages/twenty-front/src/modules/views/components/ViewBarRecordFieldEffect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarRecordFieldEffect.tsx new file mode 100644 index 0000000000..f3ec4bf963 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewBarRecordFieldEffect.tsx @@ -0,0 +1,65 @@ +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector'; +import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { hasInitializedCurrentRecordFieldsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFieldsComponentFamilyState'; +import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField'; +import { useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const ViewBarRecordFieldEffect = () => { + const currentViewId = useRecoilComponentValue( + contextStoreCurrentViewIdComponentState, + ); + + const { objectMetadataItem } = useRecordIndexContextOrThrow(); + + const currentView = useRecoilValue( + prefetchViewFromViewIdFamilySelector({ + viewId: currentViewId ?? '', + }), + ); + + const [ + hasInitializedCurrentRecordFields, + setHasInitializedCurrentRecordFields, + ] = useRecoilComponentFamilyState( + hasInitializedCurrentRecordFieldsComponentFamilyState, + { + viewId: currentViewId ?? undefined, + }, + ); + + const setCurrentRecordFields = useSetRecoilComponentState( + currentRecordFieldsComponentState, + ); + + useEffect(() => { + if (!hasInitializedCurrentRecordFields && isDefined(currentView)) { + if (currentView.objectMetadataId !== objectMetadataItem.id) { + return; + } + + const recordFields = currentView.viewFields + .map(mapViewFieldToRecordField) + .filter(isDefined); + + setCurrentRecordFields(recordFields); + + setHasInitializedCurrentRecordFields(true); + } + }, [ + currentViewId, + setCurrentRecordFields, + hasInitializedCurrentRecordFields, + setHasInitializedCurrentRecordFields, + currentView, + objectMetadataItem, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx b/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx index 3951c4c3df..385c025a5a 100644 --- a/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx +++ b/packages/twenty-front/src/modules/views/components/__stories__/ViewBarFilterDropdown.stories.tsx @@ -6,10 +6,7 @@ import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlur import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState'; @@ -20,6 +17,7 @@ import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewType } from '@/views/types/ViewType'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId'; import { type View } from '@/views/types/View'; @@ -106,33 +104,23 @@ const meta: Meta = { recordIndexId: instanceId, }} > - - - - - - - - - - - - - + + + + + + ); }, diff --git a/packages/twenty-front/src/modules/views/states/hasInitializedCurrentRecordFieldsComponentFamilyState.ts b/packages/twenty-front/src/modules/views/states/hasInitializedCurrentRecordFieldsComponentFamilyState.ts new file mode 100644 index 0000000000..34cb071eae --- /dev/null +++ b/packages/twenty-front/src/modules/views/states/hasInitializedCurrentRecordFieldsComponentFamilyState.ts @@ -0,0 +1,9 @@ +import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext'; +import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; + +export const hasInitializedCurrentRecordFieldsComponentFamilyState = + createComponentFamilyState({ + key: 'hasInitializedCurrentRecordFieldsComponentFamilyState', + defaultValue: false, + componentInstanceContext: RecordFieldsComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/views/utils/areRecordFieldsEqual.ts b/packages/twenty-front/src/modules/views/utils/areRecordFieldsEqual.ts new file mode 100644 index 0000000000..f16e608a25 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/areRecordFieldsEqual.ts @@ -0,0 +1,22 @@ +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined'; + +export const areRecordFieldsEqual = ( + recordFieldA: RecordField, + recordFieldB: RecordField, +) => { + const propertiesToCompare: (keyof RecordField)[] = [ + 'fieldMetadataItemId', + 'isVisible', + 'position', + 'size', + 'aggregateOperation', + ]; + + return propertiesToCompare.every((property) => + compareStrictlyExceptForNullAndUndefined( + recordFieldA[property], + recordFieldB[property], + ), + ); +}; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFieldToRecordField.ts b/packages/twenty-front/src/modules/views/utils/mapViewFieldToRecordField.ts new file mode 100644 index 0000000000..ba0ebcbdb0 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapViewFieldToRecordField.ts @@ -0,0 +1,15 @@ +import { type RecordField } from '@/object-record/record-field/types/RecordField'; +import { type ViewField } from '@/views/types/ViewField'; + +export const mapViewFieldToRecordField = (viewField: ViewField) => { + const recordField: RecordField = { + id: viewField.id, + fieldMetadataItemId: viewField.fieldMetadataId, + isVisible: viewField.isVisible, + position: viewField.position, + size: viewField.size, + aggregateOperation: viewField.aggregateOperation, + }; + + return recordField; +}; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index d7ac9c91f0..44f98df29a 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -5,12 +5,10 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect'; import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton'; import { PageBody } from '@/ui/layout/page/components/PageBody'; import { PageContainer } from '@/ui/layout/page/components/PageContainer'; @@ -33,55 +31,47 @@ export const RecordShowPage = () => { computeRecordShowComponentInstanceId(objectRecordId); return ( - - - - - + + - - + + + + + - - - - - - - - - - - - - - - - + loading={false} + /> + + + + + + ); }; diff --git a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx index ce7bf8d7aa..e57e462b1a 100644 --- a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx @@ -24,9 +24,7 @@ import { MainContextStoreProvider } from '@/context-store/components/MainContext import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect'; import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; -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 { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext'; import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect'; @@ -97,26 +95,11 @@ const Providers = () => { - - - - - - - + + diff --git a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx index 0396c45dff..ce794ac3ce 100644 --- a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx @@ -5,12 +5,10 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableContextProvider, @@ -145,32 +143,24 @@ export const RecordTableDecorator: Decorator = (Story, context) => { - - - - - - - - - - - - + + + + + ); diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx index 9821d06895..5b68c52cca 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper.tsx @@ -1,9 +1,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; -import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; -import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; +import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext'; import { type MockedResponse } from '@apollo/client/testing'; import { type ReactNode } from 'react'; import { type MutableSnapshot } from 'recoil'; @@ -51,58 +49,48 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({ return ({ children }: { children: ReactNode }) => ( - - - - 'indexIdentifierUrl', + onIndexRecordsLoaded: () => {}, + objectNamePlural: mockObjectMetadataItem.namePlural, + objectNameSingular: mockObjectMetadataItem.nameSingular, + objectMetadataItem: mockObjectMetadataItem, + recordIndexId: 'recordIndexId', + }} > - - 'indexIdentifierUrl', - onIndexRecordsLoaded: () => {}, - objectNamePlural: mockObjectMetadataItem.namePlural, - objectNameSingular: mockObjectMetadataItem.nameSingular, - objectMetadataItem: mockObjectMetadataItem, - recordIndexId: 'recordIndexId', - }} - > - - {children} - - - - - - - + {children} + + + + + ); }; diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx index 87faba2ec7..aacb1852b1 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx @@ -1,12 +1,10 @@ import { MockedProvider, type MockedResponse } from '@apollo/client/testing'; import { type ReactNode } from 'react'; -import { type MutableSnapshot, RecoilRoot } from 'recoil'; +import { RecoilRoot, type MutableSnapshot } from 'recoil'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; -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 { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { type InMemoryCache } from '@apollo/client'; @@ -30,31 +28,21 @@ export const getJestMetadataAndApolloMocksWrapper = ({ value={{ instanceId: 'snack-bar-manager' }} > - - - - + - - - - {children} - - - - - - - + {children} + + + +