From d7da73f0eca13642fac0de9fb3f0efe9033ef4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 12 Dec 2024 11:50:13 +0100 Subject: [PATCH] feat: record group add new (#8925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #8757 This PR is adding the Add new button on view groups. Also this PR fix an issue where the pending record can be draggable, and is causing error. Screenshot 2024-12-10 at 4 24 43 PM It also start to issues with the way we're using Context. We're initializing pretty much all Context like this: ```typescript export const RecordTableContext = createContext( {} as RecordTableContextProps, ); ``` This is causing issues when by mistake we use the context like this outside the Provider hierarchy: ```typescript const context = useContext(RecordTableContext); ``` This is going to fail silently, and all the context variables become undefined... To fix this I've introduced an util called `createRequiredContext`, this one is returning an array containing the provider and the hook to retrieve the context. The context is initialized to undefined inside this utility, this way we can check if the value has been initialized with the provider to check if we're inside it. It'll throw an error if this one is used outside the provider. The return values are properly typed, so `undefined` is not added to the value of the Context. I'll create a followup ticket to use this new utility function, if that's ok and replace it everywhere in the codebase. We can also consider adding a eslint rule to warn about the use of `createContext` directly. --- .../ObjectFilterDropdownFilterSelect.tsx | 5 +- .../MultipleFiltersDropdownButton.stories.tsx | 40 +++-- .../ObjectOptionsDropdownContent.stories.tsx | 7 +- .../useSearchRecordGroupField.test.tsx | 6 +- .../hooks/useSearchRecordGroupField.ts | 6 +- .../components/ObjectSortDropdownButton.tsx | 5 +- .../perf/ChipFieldDisplay.perf.stories.tsx | 26 +++ .../hooks/useCurrentRecordGroupId.ts | 2 +- .../hooks/useRecordGroupActions.ts | 6 +- .../record-group/hooks/useSetRecordGroup.ts | 5 +- .../components/RecordIndexContainer.tsx | 6 +- ...textStoreNumberOfSelectedRecordsEffect.tsx | 6 +- ...tainerContextStoreObjectMetadataEffect.tsx | 6 +- ...RecordIndexFiltersToContextStoreEffect.tsx | 6 +- .../components/RecordIndexPageHeader.tsx | 25 +-- .../RecordIndexPageKanbanAddButton.tsx | 40 ++--- .../RecordIndexPageTableAddButton.tsx | 16 ++ .../RecordIndexPageTableAddButtonInGroup.tsx | 80 +++++++++ .../RecordIndexPageTableAddButtonNoGroup.tsx | 21 +++ .../components/RecordIndexRecordChip.tsx | 5 +- .../components/RecordIndexTableContainer.tsx | 5 +- .../RecordIndexTableContainerEffect.tsx | 8 +- ...tPropsContext.ts => RecordIndexContext.ts} | 9 +- .../record-table/components/RecordTable.tsx | 97 ++++------- .../components/RecordTableContextProvider.tsx | 97 ++--------- ...dTableNoRecordGroupBodyContextProvider.tsx | 95 +++++++++++ ...ordTableRecordGroupBodyContextProvider.tsx | 99 +++++++++++ .../components/RecordTableRecordGroupRows.tsx | 47 +++-- .../components/RecordTableWithWrappers.tsx | 50 +++--- .../perf/RecordTableCell.perf.stories.tsx | 108 ++++++------ .../contexts/RecordTableBodyContext.ts | 36 ++++ .../contexts/RecordTableContext.ts | 36 +--- .../contexts/RecordTableRowContext.ts | 1 + .../components/RecordTableEmptyState.tsx | 7 +- .../RecordTableEmptyStateDisplay.tsx | 5 +- .../RecordTableEmptyStateNoRecordAtAll.tsx | 7 +- ...dTableEmptyStateNoRecordFoundForFilter.tsx | 7 +- .../RecordTableEmptyStateSoftDelete.tsx | 5 +- .../useUpsertTableRecordInGroup.test.tsx} | 89 ++++++---- .../useUpsertTableRecordNoGroup.test.tsx | 160 ++++++++++++++++++ .../internal/useUpsertTableRecordInGroup.ts | 87 ++++++++++ .../internal/useUpsertTableRecordNoGroup.ts} | 47 ++--- .../hooks/useCreateNewTableRecordInGroup.ts | 68 ++++++++ .../hooks/useCreateNewTableRecords.ts | 85 ++++++++-- .../record-table/hooks/useRecordTable.ts | 7 - ...RecordTableBodyDragDropContextProvider.tsx | 6 +- ...BodyRecordGroupDragDropContextProvider.tsx | 6 +- .../RecordTableBodyUnselectEffect.tsx | 5 +- .../RecordTableNoRecordGroupBody.tsx | 15 +- .../RecordTableNoRecordGroupBodyEffect.tsx | 6 +- .../RecordTableRecordGroupBodyEffect.tsx | 6 +- .../RecordTableRecordGroupsBody.tsx | 27 ++- .../RecordTableCellBaseContainer.tsx | 4 +- .../components/RecordTableCellDisplayMode.tsx | 5 +- .../RecordTableCellFieldContextWrapper.tsx | 6 +- .../components/RecordTableCellFieldInput.tsx | 7 +- .../components/RecordTableCellGrip.tsx | 28 +-- .../RecordTableCellSoftFocusMode.tsx | 6 +- .../useCloseRecordTableCellInGroup.test.tsx | 101 +++++++++++ .../useCloseRecordTableCellNoGroup.test.tsx} | 54 +++--- .../useCloseRecordTableCellInGroup.ts | 56 ++++++ .../useCloseRecordTableCellNoGroup.ts} | 24 ++- .../hooks/useCloseRecordTableCell.ts | 38 ----- .../hooks/useOpenRecordTableCellFromCell.ts | 9 +- .../hooks/useOpenRecordTableCellV2.ts | 5 +- .../components/RecordTableHeader.tsx | 19 +-- .../components/RecordTableHeaderCell.tsx | 18 +- .../RecordTableHeaderPlusButtonContent.tsx | 7 +- .../RecordTablePendingRecordGroupRow.tsx | 25 +++ .../components/RecordTableRowWrapper.tsx | 15 +- .../RecordTableRecordGroupEmptyRow.tsx | 7 + .../RecordTableRecordGroupSectionAddNew.tsx | 36 ++++ .../RecordTableRecordGroupSectionLoadMore.tsx | 5 +- ...dingRecordIdByGroupComponentFamilyState.ts | 10 ++ ...ecordTablePendingRecordIdComponentState.ts | 2 +- .../SignInBackgroundMockContainer.tsx | 7 +- .../hooks/useCreateViewFromCurrentView.ts | 5 +- .../pages/object-record/RecordIndexPage.tsx | 14 +- .../decorators/RecordTableDecorator.tsx | 30 ++-- .../src/utils/createRequiredContext.ts | 20 +++ .../src/utils/createRootPropsContext.ts | 11 -- 81 files changed, 1544 insertions(+), 682 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx rename packages/twenty-front/src/modules/object-record/record-index/contexts/{RecordIndexRootPropsContext.ts => RecordIndexContext.ts} (53%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts rename packages/twenty-front/src/modules/object-record/record-table/{record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx => hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx} (60%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts rename packages/twenty-front/src/modules/object-record/record-table/{record-table-cell/hooks/useUpsertRecord.ts => hooks/internal/useUpsertTableRecordNoGroup.ts} (65%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx rename packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/{__tests__/useCloseRecordTableCell.test.tsx => internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx} (62%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts rename packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/{useCloseRecordTableCellV2.ts => internal/useCloseRecordTableCellNoGroup.ts} (66%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState.ts create mode 100644 packages/twenty-front/src/utils/createRequiredContext.ts delete mode 100644 packages/twenty-front/src/utils/createRootPropsContext.ts diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index d879d35f6790..a86bc18ab015 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -9,7 +9,7 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdow import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; @@ -20,7 +20,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/ import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; @@ -57,7 +56,7 @@ type ObjectFilterDropdownFilterSelectProps = { export const ObjectFilterDropdownFilterSelect = ({ isAdvancedFilterButtonVisible, }: ObjectFilterDropdownFilterSelectProps) => { - const { recordIndexId } = useContext(RecordIndexRootPropsContext); + const { recordIndexId } = useRecordIndexContextOrThrow(); const { setObjectFilterDropdownSearchInput, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx index 90d85da61edf..ea06072c09be 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx @@ -1,9 +1,12 @@ import { Meta, StoryObj } from '@storybook/react'; import { TaskGroups } from '@/activities/tasks/components/TaskGroups'; +import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton'; import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; @@ -19,6 +22,7 @@ import { FieldMetadataType } from '~/generated/graphql'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const meta: Meta = { title: @@ -26,6 +30,9 @@ const meta: Meta = { component: MultipleFiltersDropdownButton, decorators: [ (Story) => { + const companyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === CoreObjectNameSingular.Company, + )!; const instanceId = 'entity-tasks-filter-scope'; const setAvailableFilterDefinitions = useSetRecoilComponentStateV2( availableFilterDefinitionsComponentState, @@ -91,19 +98,30 @@ const meta: Meta = { }, ]); return ( - '', + onIndexRecordsLoaded: () => {}, + objectNamePlural: CoreObjectNamePlural.Company, + objectNameSingular: CoreObjectNameSingular.Company, + objectMetadataItem: companyObjectMetadataItem, + recordIndexId: instanceId, + }} > - {} }} + - - - - - - - + {} }} + > + + + + + + + + ); }, ObjectMetadataItemsDecorator, 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 f53d7f995b79..ce819f7496e6 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 @@ -7,7 +7,7 @@ import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dro 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 { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; @@ -76,11 +76,10 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({ )!; return ( - '', onIndexRecordsLoaded: () => {}, - onCreateRecord: () => {}, objectNamePlural: 'companies', objectNameSingular: 'company', objectMetadataItem: companyObjectMetadataItem, @@ -102,7 +101,7 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({ - + ); }, ], diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx index caa57250eaaf..cf2ffba00fa2 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx @@ -1,5 +1,5 @@ import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { renderHook } from '@testing-library/react'; import { act } from 'react'; @@ -11,13 +11,13 @@ describe('useSearchRecordGroupField', () => { renderHook(() => useSearchRecordGroupField(), { wrapper: ({ children }) => ( - + {children} - + ), }); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts index 250554f52471..633acc0bb34e 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts @@ -1,11 +1,11 @@ import { objectOptionsDropdownSearchInputComponentState } from '@/object-record/object-options-dropdown/states/objectOptionsDropdownSearchInputComponentState'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; -import { useContext, useMemo } from 'react'; +import { useMemo } from 'react'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const useSearchRecordGroupField = () => { - const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); + const { objectMetadataItem } = useRecordIndexContextOrThrow(); const [recordGroupFieldSearchInput, setRecordGroupFieldSearchInput] = useRecoilComponentStateV2(objectOptionsDropdownSearchInputComponentState); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index ebad9ac412a3..014905a5e63c 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -5,7 +5,7 @@ import { IconChevronDown, MenuItem, useIcons } from 'twenty-ui'; import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId'; import { useObjectSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useObjectSortDropdown'; import { ObjectSortDropdownScope } from '@/object-record/object-sort-dropdown/scopes/ObjectSortDropdownScope'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -16,7 +16,6 @@ import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/Styl import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useContext } from 'react'; import { SORT_DIRECTIONS } from '../types/SortDirection'; export const StyledInput = styled.input` @@ -77,7 +76,7 @@ export const ObjectSortDropdownButton = ({ resetSearchInput, } = useObjectSortDropdown(); - const { recordIndexId } = useContext(RecordIndexRootPropsContext); + const { recordIndexId } = useRecordIndexContextOrThrow(); const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx index ad1235d24117..bc2964a29499 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx @@ -1,15 +1,41 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui'; +import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ChipFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ChipFieldDisplay'; +import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator'; import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; const meta: Meta = { title: 'UI/Data/Field/Display/ChipFieldDisplay', decorators: [ + (Story) => { + const instanceId = 'child-field-display-scope'; + + const companyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === CoreObjectNameSingular.Company, + )!; + + return ( + '', + onIndexRecordsLoaded: () => {}, + objectNamePlural: CoreObjectNamePlural.Company, + objectNameSingular: CoreObjectNameSingular.Company, + objectMetadataItem: companyObjectMetadataItem, + recordIndexId: instanceId, + }} + > + + + ); + }, MemoryRouterDecorator, ChipGeneratorsDecorator, getFieldDecorator('person', 'name'), diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts index b9960a19c6ce..721994105ef6 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts @@ -1,7 +1,7 @@ import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; import { useContext } from 'react'; -export const useCurrentRecordGroupId = () => { +export const useCurrentRecordGroupId = (): string => { const context = useContext(RecordGroupContext); if (!context) { diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts index b1bc9829d768..2c88b9609eb0 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts @@ -5,7 +5,7 @@ import { RecordBoardColumnContext } from '@/object-record/record-board/record-bo import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useCallback, useContext, useMemo } from 'react'; @@ -17,9 +17,7 @@ export const useRecordGroupActions = () => { const navigate = useNavigate(); const location = useLocation(); - const { objectNameSingular, recordIndexId } = useContext( - RecordIndexRootPropsContext, - ); + const { objectNameSingular, recordIndexId } = useRecordIndexContextOrThrow(); const { columnDefinition: recordGroupDefinition } = useContext( RecordBoardColumnContext, diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts index 934208dc5c82..37dc16c1d578 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts @@ -2,15 +2,14 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetRecordGroup = (viewId?: string) => { - const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); + const { objectMetadataItem } = useRecordIndexContextOrThrow(); const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2( recordGroupIdsComponentState, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 02bc1ed52dff..9fc7bfcbb345 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -17,7 +17,7 @@ import { recordIndexSortsState } from '@/object-record/record-index/states/recor import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; @@ -39,7 +39,7 @@ import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useCallback, useContext } from 'react'; +import { useCallback } from 'react'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; const StyledContainer = styled.div` @@ -67,7 +67,7 @@ export const RecordIndexContainer = () => { recordIndexId, objectMetadataItem, objectNameSingular, - } = useContext(RecordIndexRootPropsContext); + } = useRecordIndexContextOrThrow(); const setRecordGroup = useSetRecordGroup(recordIndexId); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx index 2c8106f6d7e6..375e5cfc02c2 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -5,11 +5,11 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { useContext, useEffect } from 'react'; +import { useEffect } from 'react'; export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = () => { @@ -21,7 +21,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = contextStoreTargetedRecordsRuleComponentState, ); - const { objectNamePlural } = useContext(RecordIndexRootPropsContext); + const { objectNamePlural } = useRecordIndexContextOrThrow(); const { objectNameSingular } = useObjectNameSingularFromPlural({ objectNamePlural, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx index 03a02ee36f10..88ad438b3a4d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx @@ -1,15 +1,15 @@ import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { useContext, useEffect } from 'react'; +import { useEffect } from 'react'; export const RecordIndexContainerContextStoreObjectMetadataEffect = () => { const setContextStoreCurrentObjectMetadataItem = useSetRecoilComponentStateV2( contextStoreCurrentObjectMetadataIdComponentState, ); - const { objectNamePlural } = useContext(RecordIndexRootPropsContext); + const { objectNamePlural } = useRecordIndexContextOrThrow(); const { objectNameSingular } = useObjectNameSingularFromPlural({ objectNamePlural, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx index 334abf9ec96c..d009c9c61390 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx @@ -1,8 +1,8 @@ -import { useContext, useEffect } from 'react'; +import { useEffect } from 'react'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; @@ -12,7 +12,7 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta import { useRecoilValue } from 'recoil'; export const RecordIndexFiltersToContextStoreEffect = () => { - const { recordIndexId } = useContext(RecordIndexRootPropsContext); + const { recordIndexId } = useRecordIndexContextOrThrow(); const recordIndexFilters = useRecoilValue(recordIndexFiltersState); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx index 3e8abc738d92..c2d6584ff391 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx @@ -3,16 +3,14 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { RecordIndexPageTableAddButton } from '@/object-record/record-index/components/RecordIndexPageTableAddButton'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton'; -import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; import { PageHeader } from '@/ui/layout/page/components/PageHeader'; -import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined, useIcons } from 'twenty-ui'; import { capitalize } from '~/utils/string/capitalize'; @@ -21,17 +19,13 @@ export const RecordIndexPageHeader = () => { const { findObjectMetadataItemByNamePlural } = useFilteredObjectMetadataItems(); - const { objectNamePlural, onCreateRecord } = useContext( - RecordIndexRootPropsContext, - ); + const { objectNamePlural } = useRecordIndexContextOrThrow(); const objectMetadataItem = findObjectMetadataItemByNamePlural(objectNamePlural); const { getIcon } = useIcons(); - const Icon = getIcon( - findObjectMetadataItemByNamePlural(objectNamePlural)?.icon, - ); + const Icon = getIcon(objectMetadataItem?.icon); const recordIndexViewType = useRecoilValue(recordIndexViewTypeState); @@ -56,17 +50,14 @@ export const RecordIndexPageHeader = () => { const pageHeaderTitle = objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural); - const handleAddButtonClick = () => { - onCreateRecord(); - }; - return ( - - {shouldDisplayAddButton && + /** + * TODO: Logic between Table and Kanban should be merged here when we move some states to record-index + */ (isTable ? ( - + ) : ( ))} diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx index c86ef48d6a5a..5bf7c3b9cacd 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx @@ -1,4 +1,3 @@ -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; @@ -6,33 +5,20 @@ import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-re import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import styled from '@emotion/styled'; -import { useCallback, useContext } from 'react'; +import { useCallback } from 'react'; import { useRecoilValue } from 'recoil'; -const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` - width: 100%; -`; - -const StyledDropDownMenu = styled(DropdownMenu)` - width: 200px; -`; - export const RecordIndexPageKanbanAddButton = () => { const dropdownId = `record-index-page-add-button-dropdown`; - const { recordIndexId, objectNameSingular } = useContext( - RecordIndexRootPropsContext, - ); - const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow(); const visibleRecordGroupIds = useRecoilComponentValueV2( visibleRecordGroupIdsComponentSelector, @@ -95,17 +81,15 @@ export const RecordIndexPageKanbanAddButton = () => { clickableComponent={} dropdownId={dropdownId} dropdownComponents={ - - - {visibleRecordGroupIds.map((recordGroupId) => ( - - ))} - - + + {visibleRecordGroupIds.map((recordGroupId) => ( + + ))} + } dropdownHotkeyScope={{ scope: dropdownId }} /> diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx new file mode 100644 index 000000000000..fe169ffe7150 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx @@ -0,0 +1,16 @@ +import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; +import { RecordIndexPageTableAddButtonInGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup'; +import { RecordIndexPageTableAddButtonNoGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordIndexPageTableAddButton = () => { + const hasRecordGroups = useRecoilComponentValueV2( + hasRecordGroupsComponentSelector, + ); + + if (!hasRecordGroups) { + return ; + } + + return ; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx new file mode 100644 index 000000000000..c40959b1fa44 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx @@ -0,0 +1,80 @@ +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useCreateNewTableRecordInGroup } from '@/object-record/record-table/hooks/useCreateNewTableRecordInGroup'; +import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useRecoilCallback } from 'recoil'; + +export const RecordIndexPageTableAddButtonInGroup = () => { + const dropdownId = `record-index-page-table-add-button-dropdown`; + + const { objectMetadataItem } = useRecordIndexContextOrThrow(); + + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); + + const isRecordGroupTableSectionToggledState = + useRecoilComponentCallbackStateV2( + isRecordGroupTableSectionToggledComponentState, + ); + + const selectFieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.id === recordGroupFieldMetadata?.id, + ); + + const { createNewTableRecordInGroup } = useCreateNewTableRecordInGroup(); + + const { closeDropdown } = useDropdown(dropdownId); + + const handleCreateNewTableRecordInGroup = useRecoilCallback( + ({ set }) => + (recordGroup: RecordGroupDefinition) => { + set(isRecordGroupTableSectionToggledState(recordGroup.id), true); + createNewTableRecordInGroup(recordGroup.id); + closeDropdown(); + }, + [ + closeDropdown, + createNewTableRecordInGroup, + isRecordGroupTableSectionToggledState, + ], + ); + + if (!selectFieldMetadataItem) { + return null; + } + + return ( + } + dropdownId={dropdownId} + dropdownComponents={ + + {visibleRecordGroupIds.map((recordGroupId) => ( + + ))} + + } + dropdownHotkeyScope={{ scope: dropdownId }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx new file mode 100644 index 000000000000..b9d4fbe65ba1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx @@ -0,0 +1,21 @@ +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; +import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect'; + +export const RecordIndexPageTableAddButtonNoGroup = () => { + const { recordIndexId } = useRecordIndexContextOrThrow(); + + const { createNewTableRecord } = useCreateNewTableRecord(recordIndexId); + + const handleCreateNewTableRecord = () => { + createNewTableRecord(); + }; + + return ( + <> + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx index b02591e8c813..c2fe3344e678 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx @@ -1,8 +1,7 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon'; import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { useContext } from 'react'; import { AvatarChip, AvatarChipVariant, ChipSize } from 'twenty-ui'; export type RecordIdentifierChipProps = { @@ -20,7 +19,7 @@ export const RecordIdentifierChip = ({ size, maxWidth, }: RecordIdentifierChipProps) => { - const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext); + const { indexIdentifierUrl } = useRecordIndexContextOrThrow(); const { recordChipData } = useRecordChipData({ objectNameSingular, record, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx index f1dec5e3e7e6..9d3d588c3862 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx @@ -1,9 +1,8 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext'; import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; -import { useContext } from 'react'; type RecordIndexTableContainerProps = { recordTableId: string; @@ -14,7 +13,7 @@ export const RecordIndexTableContainer = ({ recordTableId, viewBarId, }: RecordIndexTableContainerProps) => { - const { objectNameSingular } = useContext(RecordIndexRootPropsContext); + const { objectNameSingular } = useRecordIndexContextOrThrow(); const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index aa7e470c9c70..098c8d646743 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -1,8 +1,8 @@ -import { useContext, useEffect } from 'react'; +import { useEffect } from 'react'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; @@ -13,9 +13,7 @@ import { ViewField } from '@/views/types/ViewField'; import { useRecoilCallback } from 'recoil'; export const RecordIndexTableContainerEffect = () => { - const { recordIndexId, objectNameSingular } = useContext( - RecordIndexRootPropsContext, - ); + const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow(); const viewBarId = recordIndexId; diff --git a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts similarity index 53% rename from packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts rename to packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts index 267501019b46..cdc52ae7c8fb 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts @@ -1,15 +1,14 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { createRootPropsContext } from '~/utils/createRootPropsContext'; +import { createRequiredContext } from '~/utils/createRequiredContext'; -export type RecordIndexRootPropsContextProps = { +export type RecordIndexContextValue = { indexIdentifierUrl: (recordId: string) => string; onIndexRecordsLoaded: () => void; - onCreateRecord: () => void; objectNamePlural: string; objectNameSingular: string; objectMetadataItem: ObjectMetadataItem; recordIndexId: string; }; -export const RecordIndexRootPropsContext = - createRootPropsContext(); +export const [RecordIndexContextProvider, useRecordIndexContextOrThrow] = + createRequiredContext('RecordIndexContext'); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 4d6f5331bccc..cc428d1980c6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -3,10 +3,9 @@ import { isNonEmptyString, isNull } from '@sniptt/guards'; import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; -import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; -import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect'; @@ -29,19 +28,9 @@ const StyledTable = styled.table` width: 100%; `; -type RecordTableProps = { - viewBarId: string; - recordTableId: string; - objectNameSingular: string; - onColumnsChange: (columns: any) => void; -}; +export const RecordTable = () => { + const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow(); -export const RecordTable = ({ - viewBarId, - recordTableId, - objectNameSingular, - onColumnsChange, -}: RecordTableProps) => { const tableBodyRef = useRef(null); const { toggleClickOutsideListener } = useClickOutsideListener( @@ -82,51 +71,39 @@ export const RecordTable = ({ } return ( - - - {!hasRecordGroups ? ( - - ) : ( - - )} - - {recordTableIsEmpty ? ( - - ) : ( - <> - - - {!hasRecordGroups ? ( - - ) : ( - - )} - - - { - resetTableRowSelection(); - toggleClickOutsideListener(false); - }} - onDragSelectionChange={setRowSelected} - onDragSelectionEnd={() => { - toggleClickOutsideListener(true); - }} - /> - - )} - - + <> + {!hasRecordGroups ? ( + + ) : ( + + )} + + {recordTableIsEmpty ? ( + + ) : ( + <> + + + {!hasRecordGroups ? ( + + ) : ( + + )} + + + { + resetTableRowSelection(); + toggleClickOutsideListener(false); + }} + onDragSelectionChange={setRowSelected} + onDragSelectionEnd={() => { + toggleClickOutsideListener(true); + }} + /> + + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx index 5ad568dc0879..d7720d129f0e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx @@ -1,117 +1,44 @@ import { ReactNode } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; -import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; -import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; -import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2'; -import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2'; -import { - OpenTableCellArgs, - useOpenRecordTableCellV2, -} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; -import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown'; -import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord'; +import { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext'; + import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; -import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; -import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +type RecordTableContextProviderProps = { + viewBarId: string; + recordTableId: string; + objectNameSingular: string; + children: ReactNode; +}; + export const RecordTableContextProvider = ({ viewBarId, recordTableId, objectNameSingular, children, -}: { - viewBarId: string; - recordTableId: string; - objectNameSingular: string; - children: ReactNode; -}) => { +}: RecordTableContextProviderProps) => { const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); - const { upsertRecord } = useUpsertRecord({ - objectNameSingular, - recordTableId, - }); - - const handleUpsertRecord = ({ - persistField, - recordId, - fieldName, - }: { - persistField: () => void; - recordId: string; - fieldName: string; - }) => { - upsertRecord(persistField, recordId, fieldName); - }; - - const { openTableCell } = useOpenRecordTableCellV2(recordTableId); - - const handleOpenTableCell = (args: OpenTableCellArgs) => { - openTableCell(args); - }; - - const { moveFocus } = useRecordTableMoveFocus(recordTableId); - - const handleMoveFocus = (direction: MoveFocusDirection) => { - moveFocus(direction); - }; - - const { closeTableCell } = useCloseRecordTableCellV2(recordTableId); - - const handleCloseTableCell = () => { - closeTableCell(); - }; - - const { moveSoftFocusToCell } = - useMoveSoftFocusToCellOnHoverV2(recordTableId); - - const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => { - moveSoftFocusToCell(cellPosition); - }; - - const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({ - recordTableId, - }); - - const handleActionMenuDropdown = ( - event: React.MouseEvent, - recordId: string, - ) => { - triggerActionMenuDropdown(event, recordId); - }; - - const { handleContainerMouseEnter } = useHandleContainerMouseEnter({ - recordTableId, - }); - const visibleTableColumns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, recordTableId, ); return ( - {children} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx new file mode 100644 index 000000000000..c88ff0ac1973 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx @@ -0,0 +1,95 @@ +import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; +import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup'; +import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; +import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup'; +import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2'; +import { + OpenTableCellArgs, + useOpenRecordTableCellV2, +} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown'; +import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; +import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; +import { ReactNode } from 'react'; + +type RecordTableNoRecordGroupBodyContextProviderProps = { + children?: ReactNode; +}; + +export const RecordTableNoRecordGroupBodyContextProvider = ({ + children, +}: RecordTableNoRecordGroupBodyContextProviderProps) => { + const { recordTableId } = useRecordTableContextOrThrow(); + + const { upsertTableRecordNoGroup } = useUpsertTableRecordNoGroup(); + + const handleUpsertTableRecordNoRecordGroup = ({ + persistField, + recordId, + fieldName, + }: { + persistField: () => void; + recordId: string; + fieldName: string; + }) => { + upsertTableRecordNoGroup(persistField, recordId, fieldName); + }; + + const { openTableCell } = useOpenRecordTableCellV2(recordTableId); + + const handleOpenTableCell = (args: OpenTableCellArgs) => { + openTableCell(args); + }; + + const { moveFocus } = useRecordTableMoveFocus(recordTableId); + + const handleMoveFocus = (direction: MoveFocusDirection) => { + moveFocus(direction); + }; + + const { closeTableCellNoGroup } = useCloseRecordTableCellNoGroup(); + + const handleCloseTableCell = () => { + closeTableCellNoGroup(); + }; + + const { moveSoftFocusToCell } = + useMoveSoftFocusToCellOnHoverV2(recordTableId); + + const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => { + moveSoftFocusToCell(cellPosition); + }; + + const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({ + recordTableId, + }); + + const handleActionMenuDropdown = ( + event: React.MouseEvent, + recordId: string, + ) => { + triggerActionMenuDropdown(event, recordId); + }; + + const { handleContainerMouseEnter } = useHandleContainerMouseEnter({ + recordTableId, + }); + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx new file mode 100644 index 000000000000..002a61624a95 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx @@ -0,0 +1,99 @@ +import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; +import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup'; +import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus'; +import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup'; +import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2'; +import { + OpenTableCellArgs, + useOpenRecordTableCellV2, +} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown'; +import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; +import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; +import { ReactNode } from 'react'; + +type RecordTableRecordGroupBodyContextProviderProps = { + recordGroupId: string; + children?: ReactNode; +}; + +export const RecordTableRecordGroupBodyContextProvider = ({ + recordGroupId, + children, +}: RecordTableRecordGroupBodyContextProviderProps) => { + const { recordTableId } = useRecordTableContextOrThrow(); + + const { upsertTableRecordInGroup } = + useUpsertTableRecordInGroup(recordGroupId); + + const handleupsertTableRecordInGroup = ({ + persistField, + recordId, + fieldName, + }: { + persistField: () => void; + recordId: string; + fieldName: string; + }) => { + upsertTableRecordInGroup(persistField, recordId, fieldName); + }; + + const { openTableCell } = useOpenRecordTableCellV2(recordTableId); + + const handleOpenTableCell = (args: OpenTableCellArgs) => { + openTableCell(args); + }; + + const { moveFocus } = useRecordTableMoveFocus(recordTableId); + + const handleMoveFocus = (direction: MoveFocusDirection) => { + moveFocus(direction); + }; + + const { closeTableCellInGroup } = + useCloseRecordTableCellInGroup(recordGroupId); + + const handlecloseTableCellInGroup = () => { + closeTableCellInGroup(); + }; + + const { moveSoftFocusToCell } = + useMoveSoftFocusToCellOnHoverV2(recordTableId); + + const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => { + moveSoftFocusToCell(cellPosition); + }; + + const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({ + recordTableId, + }); + + const handleActionMenuDropdown = ( + event: React.MouseEvent, + recordId: string, + ) => { + triggerActionMenuDropdown(event, recordId); + }; + + const { handleContainerMouseEnter } = useHandleContainerMouseEnter({ + recordTableId, + }); + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx index 01688556de96..c944fd92a27f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx @@ -1,7 +1,10 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; +import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew'; +import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore'; import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -30,24 +33,32 @@ export const RecordTableRecordGroupRows = () => { [allRecordIds], ); + if (!isRecordGroupTableSectionToggled) { + return null; + } + return ( - isRecordGroupTableSectionToggled && - recordIdsByGroup.map((recordId, rowIndexInGroup) => { - const rowIndex = rowIndexMap.get(recordId); - - if (!isDefined(rowIndex)) { - return null; - } - - return ( - - ); - }) + <> + {recordIdsByGroup.map((recordId, rowIndexInGroup) => { + const rowIndex = rowIndexMap.get(recordId); + + if (!isDefined(rowIndex)) { + return null; + } + + return ( + + ); + })} + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index d00e81bc3d0f..9273985454c9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -16,6 +16,8 @@ import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext import { useRecordTable } from '../hooks/useRecordTable'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; +import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; +import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { Key } from 'ts-key-enum'; @@ -96,27 +98,33 @@ export const RecordTableWithWrappers = ({ ); return ( - - + - - - - - - - - - - - + + + + + + + + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index 6b2a39237f5c..f2f99633ed45 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -14,12 +14,13 @@ import { import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; +import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; +import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { mockPerformance } from './mock'; @@ -61,78 +62,83 @@ const meta: Meta = { (Story) => { return ( - {}, - onOpenTableCell: () => {}, - onMoveFocus: () => {}, - onCloseTableCell: () => {}, - onMoveSoftFocusToCell: () => {}, - onActionMenuDropdownOpened: () => {}, - onCellMouseEnter: () => {}, visibleTableColumns: mockPerformance.visibleTableColumns as any, objectNameSingular: mockPerformance.objectMetadataItem.nameSingular, - recordTableId: 'recordTableId', }} > {}} > - {}, + onOpenTableCell: () => {}, + onMoveFocus: () => {}, + onCloseTableCell: () => {}, + onMoveSoftFocusToCell: () => {}, + onActionMenuDropdownOpened: () => {}, + onCellMouseEnter: () => {}, }} > - - - - - - - - - -
-
-
-
+ + + + + + + + +
+
+ + +
-
+
); }, diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts new file mode 100644 index 000000000000..4884c8e25b75 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts @@ -0,0 +1,36 @@ +import React from 'react'; + +import { HandleContainerMouseEnterArgs } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; +import { OpenTableCellArgs } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; +import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; +import { createRequiredContext } from '~/utils/createRequiredContext'; + +export type RecordTableBodyContextProps = { + recordGroupId?: string; + onUpsertRecord: ({ + persistField, + recordId, + fieldName, + }: { + persistField: () => void; + recordId: string; + fieldName: string; + }) => void; + onOpenTableCell: (args: OpenTableCellArgs) => void; + onMoveFocus: (direction: MoveFocusDirection) => void; + onCloseTableCell: () => void; + onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void; + onActionMenuDropdownOpened: ( + event: React.MouseEvent, + recordId: string, + ) => void; + onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void; +}; + +export const [ + RecordTableBodyContextProvider, + useRecordTableBodyContextOrThrow, +] = createRequiredContext( + 'RecordTableBodyContext', +); diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts index ba2ea9f7a92d..09fadd7f2032 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts @@ -1,39 +1,15 @@ -import React, { createContext } from 'react'; - import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { HandleContainerMouseEnterArgs } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter'; -import { OpenTableCellArgs } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; -import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; +import { createRequiredContext } from '~/utils/createRequiredContext'; -export type RecordTableContextProps = { +export type RecordTableContextValue = { + recordTableId: string; viewBarId: string; + objectNameSingular: string; objectMetadataItem: ObjectMetadataItem; - onUpsertRecord: ({ - persistField, - recordId, - fieldName, - }: { - persistField: () => void; - recordId: string; - fieldName: string; - }) => void; - onOpenTableCell: (args: OpenTableCellArgs) => void; - onMoveFocus: (direction: MoveFocusDirection) => void; - onCloseTableCell: () => void; - onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void; - onActionMenuDropdownOpened: ( - event: React.MouseEvent, - recordId: string, - ) => void; - onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void; visibleTableColumns: ColumnDefinition[]; - recordTableId: string; - objectNameSingular: string; }; -export const RecordTableContext = createContext( - {} as RecordTableContextProps, -); +export const [RecordTableContextProvider, useRecordTableContextOrThrow] = + createRequiredContext('RecordTableContext'); diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts index ac7f91a6bcb1..3ff64f7adc3a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts @@ -11,6 +11,7 @@ export type RecordTableRowContextProps = { isDragging: boolean; dragHandleProps: DraggableProvidedDragHandleProps | null; inView?: boolean; + isDragDisabled?: boolean; }; export const RecordTableRowContext = createContext( diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx index 47988fbc57c7..680ab7b40684 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx @@ -1,16 +1,15 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll'; import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter'; import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote'; import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete'; import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useContext } from 'react'; export const RecordTableEmptyState = () => { - const { objectNameSingular, recordTableId, objectMetadataItem } = - useContext(RecordTableContext); + const { recordTableId, objectNameSingular, objectMetadataItem } = + useRecordTableContextOrThrow(); const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 }); const noRecordAtAll = totalCount === 0; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index 1d97ed9f461b..8b12987a0488 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -1,6 +1,5 @@ import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; -import { useContext } from 'react'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, @@ -29,7 +28,7 @@ export const RecordTableEmptyStateDisplay = ({ subTitle, title, }: RecordTableEmptyStateDisplayProps) => { - const { objectMetadataItem } = useContext(RecordTableContext); + const { objectMetadataItem } = useRecordTableContextOrThrow(); const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem); return ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx index 00678d00ec30..5fdb72c75d55 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx @@ -1,15 +1,14 @@ import { IconPlus } from 'twenty-ui'; import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; -import { useContext } from 'react'; export const RecordTableEmptyStateNoRecordAtAll = () => { - const { createNewTableRecord } = useCreateNewTableRecord(); + const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow(); - const { objectMetadataItem } = useContext(RecordTableContext); + const { createNewTableRecord } = useCreateNewTableRecord(recordTableId); const handleButtonClick = () => { createNewTableRecord(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx index f733a636ddcc..3debe89d9a38 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx @@ -1,15 +1,14 @@ import { IconPlus } from 'twenty-ui'; import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; -import { useContext } from 'react'; export const RecordTableEmptyStateNoRecordFoundForFilter = () => { - const { createNewTableRecord } = useCreateNewTableRecord(); + const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow(); - const { objectMetadataItem } = useContext(RecordTableContext); + const { createNewTableRecord } = useCreateNewTableRecord(recordTableId); const handleButtonClick = () => { createNewTableRecord(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx index df31ce6a36ca..1321aca74a2e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx @@ -2,16 +2,15 @@ import { IconFilterOff } from 'twenty-ui'; import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel'; import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; -import { useContext } from 'react'; export const RecordTableEmptyStateSoftDelete = () => { const { objectMetadataItem, objectNameSingular, recordTableId } = - useContext(RecordTableContext); + useRecordTableContextOrThrow(); const { deleteCombinedViewFilter } = useDeleteCombinedViewFilters(recordTableId); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx similarity index 60% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx rename to packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx index 779061ecd9c0..ee690ec18b88 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx @@ -1,5 +1,5 @@ -import { act, renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; +import { renderHook } from '@testing-library/react'; +import { ReactNode, act } from 'react'; import { RecoilRoot } from 'recoil'; import { createState } from 'twenty-ui'; @@ -9,12 +9,16 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord'; +import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; +import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup'; +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const draftValue = 'updated Name'; +const recordGroupId = 'recordGroupId'; // Todo refactor this test to inject the states in a cleaner way instead of mocking hooks // (this is not easy to maintain while refactoring) @@ -37,8 +41,11 @@ jest.mock( }), ); -const pendingRecordIdState = createState({ - key: 'pendingRecordIdState', +const recordTablePendingRecordIdByGroupComponentFamilyState = createFamilyState< + string | null, + string +>({ + key: 'recordTablePendingRecordIdByGroupComponentFamilyState', defaultValue: null, }); @@ -60,46 +67,64 @@ const Wrapper = ({ { snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems); - snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue); + snapshot.set( + recordTablePendingRecordIdByGroupComponentFamilyState(recordGroupId), + pendingRecordIdMockedValue, + ); snapshot.set(draftValueState, draftValueMockedValue); }} > - - - {children} - - + + + {children} + + + + ); -describe('useUpsertRecord', () => { +describe('useUpsertTableRecordInGroup', () => { beforeEach(async () => { createOneRecordMock.mockClear(); updateOneRecordMock.mockClear(); }); it('calls update record if there is no pending record', async () => { - const { result } = renderHook( - () => - useUpsertRecord({ + /** + * { objectNameSingular: 'person', recordTableId: 'recordTableId', - }), + } + */ + const { result } = renderHook( + () => useUpsertTableRecordInGroup(recordGroupId), { wrapper: ({ children }) => Wrapper({ @@ -111,7 +136,7 @@ describe('useUpsertRecord', () => { ); await act(async () => { - await result.current.upsertRecord( + await result.current.upsertTableRecordInGroup( updateOneRecordMock, 'recordId', 'name', @@ -124,11 +149,7 @@ describe('useUpsertRecord', () => { it('calls update record if pending record is empty', async () => { const { result } = renderHook( - () => - useUpsertRecord({ - objectNameSingular: 'person', - recordTableId: 'recordTableId', - }), + () => useUpsertTableRecordInGroup(recordGroupId), { wrapper: ({ children }) => Wrapper({ @@ -140,7 +161,7 @@ describe('useUpsertRecord', () => { ); await act(async () => { - await result.current.upsertRecord( + await result.current.upsertTableRecordInGroup( updateOneRecordMock, 'recordId', 'name', diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx new file mode 100644 index 000000000000..d1f29e2c9afe --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx @@ -0,0 +1,160 @@ +import { renderHook } from '@testing-library/react'; +import { ReactNode, act } from 'react'; +import { RecoilRoot } from 'recoil'; +import { createState } from 'twenty-ui'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; +import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup'; +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +const draftValue = 'updated Name'; + +// Todo refactor this test to inject the states in a cleaner way instead of mocking hooks +// (this is not easy to maintain while refactoring) +jest.mock('@/object-record/hooks/useCreateOneRecord', () => ({ + __esModule: true, + useCreateOneRecord: jest.fn(), +})); + +const draftValueState = createState({ + key: 'draftValueState', + defaultValue: null, +}); +jest.mock( + '@/object-record/record-field/hooks/internal/useRecordFieldInputStates', + () => ({ + __esModule: true, + useRecordFieldInputStates: jest.fn(() => ({ + getDraftValueSelector: () => draftValueState, + })), + }), +); + +const pendingRecordIdState = createState({ + key: 'pendingRecordIdState', + defaultValue: null, +}); + +const createOneRecordMock = jest.fn(); +const updateOneRecordMock = jest.fn(); +(useCreateOneRecord as jest.Mock).mockReturnValue({ + createOneRecord: createOneRecordMock, +}); + +const Wrapper = ({ + children, + pendingRecordIdMockedValue, + draftValueMockedValue, +}: { + children: ReactNode; + pendingRecordIdMockedValue: string | null; + draftValueMockedValue: string | null; +}) => ( + { + snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems); + snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue); + snapshot.set(draftValueState, draftValueMockedValue); + }} + > + + + + + {children} + + + + + +); + +describe('useUpsertTableRecordNoGroup', () => { + beforeEach(async () => { + createOneRecordMock.mockClear(); + updateOneRecordMock.mockClear(); + }); + + it('calls update record if there is no pending record', async () => { + /** + * { + objectNameSingular: 'person', + recordTableId: 'recordTableId', + } + */ + const { result } = renderHook(() => useUpsertTableRecordNoGroup(), { + wrapper: ({ children }) => + Wrapper({ + pendingRecordIdMockedValue: null, + draftValueMockedValue: null, + children, + }), + }); + + await act(async () => { + await result.current.upsertTableRecordNoGroup( + updateOneRecordMock, + 'recordId', + 'name', + ); + }); + + expect(createOneRecordMock).not.toHaveBeenCalled(); + expect(updateOneRecordMock).toHaveBeenCalled(); + }); + + it('calls update record if pending record is empty', async () => { + const { result } = renderHook(() => useUpsertTableRecordNoGroup(), { + wrapper: ({ children }) => + Wrapper({ + pendingRecordIdMockedValue: null, + draftValueMockedValue: draftValue, + children, + }), + }); + + await act(async () => { + await result.current.upsertTableRecordNoGroup( + updateOneRecordMock, + 'recordId', + 'name', + ); + }); + + expect(createOneRecordMock).not.toHaveBeenCalled(); + expect(updateOneRecordMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts new file mode 100644 index 000000000000..cabcf7e6e681 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts @@ -0,0 +1,87 @@ +import { useRecoilCallback } from 'recoil'; + +import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState'; +import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector'; +import { isDefined } from '~/utils/isDefined'; + +export const useUpsertTableRecordInGroup = (recordGroupId: string) => { + const { objectMetadataItem, objectNameSingular } = + useRecordTableContextOrThrow(); + + const { createOneRecord } = useCreateOneRecord({ + objectNameSingular, + shouldMatchRootQueryFilter: true, + }); + + const recordTablePendingRecordIdByGroupFamilyState = + useRecoilComponentCallbackStateV2( + recordTablePendingRecordIdByGroupComponentFamilyState, + ); + + const upsertTableRecordInGroup = useRecoilCallback( + ({ snapshot }) => + (persistField: () => void, recordId: string, fieldName: string) => { + const labelIdentifierFieldMetadataItem = + getLabelIdentifierFieldMetadataItem(objectMetadataItem); + + const fieldScopeId = getScopeIdFromComponentId( + `${recordId}-${fieldName}`, + ); + + const draftValueSelector = extractComponentSelector( + recordFieldInputDraftValueComponentSelector, + fieldScopeId, + ); + + const draftValue = getSnapshotValue(snapshot, draftValueSelector()); + + // We're in a record group + const recordTablePendingRecordId = getSnapshotValue( + snapshot, + recordTablePendingRecordIdByGroupFamilyState(recordGroupId), + ); + + const recordGroupDefinition = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroupId), + ); + + const recordGroupFieldMetadataItem = objectMetadataItem.fields.find( + (fieldMetadata) => + fieldMetadata.id === recordGroupDefinition?.fieldMetadataId, + ); + + if ( + isDefined(recordTablePendingRecordId) && + isDefined(recordGroupDefinition) && + isDefined(recordGroupFieldMetadataItem) && + isDefined(draftValue) + ) { + createOneRecord({ + id: recordTablePendingRecordId, + [labelIdentifierFieldMetadataItem?.name ?? 'name']: draftValue, + [recordGroupFieldMetadataItem.name]: recordGroupDefinition.value, + position: 'first', + }); + } else if (!recordTablePendingRecordId) { + persistField(); + } + }, + [ + createOneRecord, + objectMetadataItem, + recordGroupId, + recordTablePendingRecordIdByGroupFamilyState, + ], + ); + + return { upsertTableRecordInGroup }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup.ts similarity index 65% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts rename to packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup.ts index 083f244fc58d..c78a47d1edec 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup.ts @@ -1,32 +1,22 @@ import { useRecoilCallback } from 'recoil'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector'; -import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector'; import { isDefined } from '~/utils/isDefined'; -export const useUpsertRecord = ({ - objectNameSingular, - recordTableId, -}: { - objectNameSingular: string; - recordTableId: string; -}) => { - const hasRecordGroups = useRecoilComponentValueV2( - hasRecordGroupsComponentSelector, - ); +export const useUpsertTableRecordNoGroup = () => { + const { objectMetadataItem, objectNameSingular, recordTableId } = + useRecordTableContextOrThrow(); const { createOneRecord } = useCreateOneRecord({ objectNameSingular, - shouldMatchRootQueryFilter: hasRecordGroups, }); const recordTablePendingRecordIdState = useRecoilComponentCallbackStateV2( @@ -34,28 +24,12 @@ export const useUpsertRecord = ({ recordTableId, ); - const upsertRecord = useRecoilCallback( + const upsertTableRecordNoGroup = useRecoilCallback( ({ snapshot }) => (persistField: () => void, recordId: string, fieldName: string) => { - const objectMetadataItems = snapshot - .getLoadable(objectMetadataItemsState) - .getValue(); - - const foundObjectMetadataItem = objectMetadataItems.find( - (item) => item.nameSingular === objectNameSingular, - ); - - if (!foundObjectMetadataItem) { - throw new Error('Object metadata item cannot be found'); - } - const labelIdentifierFieldMetadataItem = - getLabelIdentifierFieldMetadataItem(foundObjectMetadataItem); + getLabelIdentifierFieldMetadataItem(objectMetadataItem); - const recordTablePendingRecordId = getSnapshotValue( - snapshot, - recordTablePendingRecordIdState, - ); const fieldScopeId = getScopeIdFromComponentId( `${recordId}-${fieldName}`, ); @@ -67,6 +41,11 @@ export const useUpsertRecord = ({ const draftValue = getSnapshotValue(snapshot, draftValueSelector()); + const recordTablePendingRecordId = getSnapshotValue( + snapshot, + recordTablePendingRecordIdState, + ); + if (isDefined(recordTablePendingRecordId) && isDefined(draftValue)) { createOneRecord({ id: recordTablePendingRecordId, @@ -77,8 +56,8 @@ export const useUpsertRecord = ({ persistField(); } }, - [createOneRecord, objectNameSingular, recordTablePendingRecordIdState], + [createOneRecord, objectMetadataItem, recordTablePendingRecordIdState], ); - return { upsertRecord }; + return { upsertTableRecordNoGroup }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts new file mode 100644 index 000000000000..8e2f0236474e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts @@ -0,0 +1,68 @@ +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode'; +import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState'; +import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; +import { v4 } from 'uuid'; +import { isDefined } from '~/utils/isDefined'; + +export const useCreateNewTableRecordInGroup = () => { + const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow(); + + const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({ + scopeId: recordIndexId, + }); + + const setHotkeyScope = useSetHotkeyScope(); + + const recordTablePendingRecordIdByGroupFamilyState = + useRecoilComponentCallbackStateV2( + recordTablePendingRecordIdByGroupComponentFamilyState, + recordIndexId, + ); + + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + + const createNewTableRecordInGroup = useRecoilCallback( + ({ set }) => + (recordGroupId: string) => { + const recordId = v4(); + + set( + recordTablePendingRecordIdByGroupFamilyState(recordGroupId), + recordId, + ); + setSelectedTableCellEditMode(-1, 0); + setHotkeyScope( + DEFAULT_CELL_SCOPE.scope, + DEFAULT_CELL_SCOPE.customScopes, + ); + + if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) { + setActiveDropdownFocusIdAndMemorizePrevious( + getDropdownFocusIdForRecordField( + recordId, + objectMetadataItem.labelIdentifierFieldMetadataId, + 'table-cell', + ), + ); + } + }, + [ + objectMetadataItem, + recordTablePendingRecordIdByGroupFamilyState, + setActiveDropdownFocusIdAndMemorizePrevious, + setHotkeyScope, + setSelectedTableCellEditMode, + ], + ); + + return { + createNewTableRecordInGroup, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts index 15056d35102d..8e3739fdcdf6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts @@ -1,33 +1,94 @@ -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; -import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode'; +import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState'; +import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; +import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { useContext } from 'react'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useRecoilCallback } from 'recoil'; import { v4 } from 'uuid'; +import { isDefined } from '~/utils/isDefined'; -export const useCreateNewTableRecord = (recordTableIdFromProps?: string) => { - const { recordTableId } = useContext(RecordTableContext); - - const recordTableIdToUse = recordTableIdFromProps ?? recordTableId; +export const useCreateNewTableRecord = (recordTableId: string) => { + const { objectMetadataItem } = useRecordIndexContextOrThrow(); const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({ - scopeId: recordTableIdToUse, + scopeId: recordTableId, }); const setHotkeyScope = useSetHotkeyScope(); - const { setPendingRecordId } = useRecordTable({ - recordTableId: recordTableIdToUse, - }); + const setPendingRecordId = useSetRecoilComponentStateV2( + recordTablePendingRecordIdComponentState, + recordTableId, + ); + + const recordTablePendingRecordIdByGroupFamilyState = + useRecoilComponentCallbackStateV2( + recordTablePendingRecordIdByGroupComponentFamilyState, + recordTableId, + ); + + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); const createNewTableRecord = () => { - setPendingRecordId(v4()); + const recordId = v4(); + + setPendingRecordId(recordId); setSelectedTableCellEditMode(-1, 0); setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes); + + if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) { + setActiveDropdownFocusIdAndMemorizePrevious( + getDropdownFocusIdForRecordField( + recordId, + objectMetadataItem.labelIdentifierFieldMetadataId, + 'table-cell', + ), + ); + } }; + const createNewTableRecordInGroup = useRecoilCallback( + ({ set }) => + (recordGroupId: string) => { + const recordId = v4(); + + set( + recordTablePendingRecordIdByGroupFamilyState(recordGroupId), + recordId, + ); + setSelectedTableCellEditMode(-1, 0); + setHotkeyScope( + DEFAULT_CELL_SCOPE.scope, + DEFAULT_CELL_SCOPE.customScopes, + ); + + if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) { + setActiveDropdownFocusIdAndMemorizePrevious( + getDropdownFocusIdForRecordField( + recordId, + objectMetadataItem.labelIdentifierFieldMetadataId, + 'table-cell', + ), + ); + } + }, + [ + objectMetadataItem, + recordTablePendingRecordIdByGroupFamilyState, + setActiveDropdownFocusIdAndMemorizePrevious, + setHotkeyScope, + setSelectedTableCellEditMode, + ], + ); + return { createNewTableRecord, + createNewTableRecordInGroup, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 65036a78e1c2..609c3f9080ac 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -22,7 +22,6 @@ import { onColumnsChangeComponentState } from '@/object-record/record-table/stat import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState'; import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState'; import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState'; -import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState'; import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState'; @@ -240,11 +239,6 @@ export const useRecordTable = (props?: useRecordTableProps) => { const isSomeCellInEditModeState = useGetIsSomeCellInEditModeState(recordTableId); - const setPendingRecordId = useSetRecoilComponentStateV2( - recordTablePendingRecordIdComponentState, - recordTableId, - ); - return { onColumnsChange, setAvailableTableColumns, @@ -272,6 +266,5 @@ export const useRecordTable = (props?: useRecordTableProps) => { setHasUserSelectedAllRows, setOnToggleColumnFilter, setOnToggleColumnSort, - setPendingRecordId, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx index aced5e0b3c38..dc74cff107d7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx @@ -1,12 +1,12 @@ import { DragDropContext, DropResult } from '@hello-pangea/dnd'; -import { ReactNode, useContext } from 'react'; +import { ReactNode } from 'react'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -18,7 +18,7 @@ export const RecordTableBodyDragDropContextProvider = ({ }: { children: ReactNode; }) => { - const { objectNameSingular, recordTableId } = useContext(RecordTableContext); + const { objectNameSingular, recordTableId } = useRecordTableContextOrThrow(); const { updateOneRecord: updateOneRow } = useUpdateOneRecord({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx index 6e229447c9fd..98474d1efa9c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx @@ -1,5 +1,5 @@ import { DragDropContext, DropResult } from '@hello-pangea/dnd'; -import { ReactNode, useContext } from 'react'; +import { ReactNode } from 'react'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; @@ -7,7 +7,7 @@ import { getDraggedRecordPosition } from '@/object-record/record-board/utils/get import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; @@ -20,7 +20,7 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({ children: ReactNode; }) => { const { objectNameSingular, recordTableId, objectMetadataItem } = - useContext(RecordTableContext); + useRecordTableContextOrThrow(); const { updateOneRecord: updateOneRow } = useUpdateOneRecord({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx index 428da6e04ec1..4381106a8475 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx @@ -1,6 +1,7 @@ import { Key } from 'ts-key-enum'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; @@ -9,13 +10,13 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis type RecordTableBodyUnselectEffectProps = { tableBodyRef: React.RefObject; - recordTableId: string; }; export const RecordTableBodyUnselectEffect = ({ tableBodyRef, - recordTableId, }: RecordTableBodyUnselectEffectProps) => { + const { recordTableId } = useRecordTableContextOrThrow(); + const leaveTableFocus = useLeaveTableFocus(recordTableId); const { resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx index 47e7e5f8f171..dec939125df0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx @@ -1,4 +1,5 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { RecordTableNoRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider'; import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/components/RecordTableNoRecordGroupRows'; import { RecordTableBodyDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; @@ -21,11 +22,13 @@ export const RecordTableNoRecordGroupBody = () => { } return ( - - - - - - + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx index f2c2fb0f6ff7..8e3466afd336 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { useDebouncedCallback } from 'use-debounce'; @@ -6,7 +6,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2'; import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState'; import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState'; @@ -18,7 +18,7 @@ import { isNonEmptyString, isNull } from '@sniptt/guards'; import { useScrollToPosition } from '~/hooks/useScrollToPosition'; export const RecordTableNoRecordGroupBodyEffect = () => { - const { objectNameSingular } = useContext(RecordTableContext); + const { objectNameSingular } = useRecordTableContextOrThrow(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx index 90569f73e345..e68e4ad15d00 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -7,13 +7,13 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useC import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState'; import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; import { isNonEmptyString, isNull } from '@sniptt/guards'; import { useScrollToPosition } from '~/hooks/useScrollToPosition'; export const RecordTableRecordGroupBodyEffect = () => { - const { objectNameSingular } = useContext(RecordTableContext); + const { objectNameSingular } = useRecordTableContextOrThrow(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx index 6cfa2c722ad6..4d1a5d544e4e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx @@ -1,13 +1,13 @@ import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; +import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider'; import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider'; -import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; +import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow'; import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection'; -import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -30,20 +30,19 @@ export const RecordTableRecordGroupsBody = () => { return ( - - - - {visibleRecordGroupIds.map((recordGroupId) => ( - ( + - - - - - - + {index > 0 && } + + + + + + + ))} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx index b7ef7b3934b9..eb6cd16dc65d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx @@ -5,8 +5,8 @@ import { BORDER_COMMON, ThemeContext } from 'twenty-ui'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext'; +import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { DEFAULT_CELL_SCOPE, useOpenRecordTableCellFromCell, @@ -47,7 +47,7 @@ export const RecordTableCellBaseContainer = ({ const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext); const { onMoveSoftFocusToCell, onCellMouseEnter } = - useContext(RecordTableContext); + useRecordTableBodyContextOrThrow(); const handleContainerMouseMove = () => { setIsFocused(true); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx index 868990a61ff7..6814c021e76a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx @@ -1,5 +1,5 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { useContext } from 'react'; import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer'; @@ -7,9 +7,10 @@ export const RecordTableCellDisplayMode = ({ children, softFocus, }: React.PropsWithChildren<{ softFocus?: boolean }>) => { - const { onActionMenuDropdownOpened } = useContext(RecordTableContext); const { recordId } = useContext(FieldContext); + const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow(); + const handleActionMenuDropdown = (event: React.MouseEvent) => { onActionMenuDropdownOpened(event, recordId); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx index 2f35fb265200..d04920b67014 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx @@ -6,7 +6,7 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; @@ -18,8 +18,10 @@ export const RecordTableCellFieldContextWrapper = ({ }: { children: ReactNode; }) => { - const { objectMetadataItem } = useContext(RecordTableContext); + const { objectMetadataItem } = useRecordTableContextOrThrow(); + const { columnDefinition } = useContext(RecordTableCellContext); + const { recordId, pathToShowPage } = useContext(RecordTableRowContext); const updateRecord = useContext(RecordUpdateContext); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx index beba813cceba..c482690b3b41 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx @@ -4,17 +4,18 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; import { useRecoilCallback } from 'recoil'; export const RecordTableCellFieldInput = () => { + const { recordId, fieldDefinition } = useContext(FieldContext); + const { onUpsertRecord, onMoveFocus, onCloseTableCell } = - useContext(RecordTableContext); + useRecordTableBodyContextOrThrow(); - const { recordId, fieldDefinition } = useContext(FieldContext); const isFieldReadOnly = useIsFieldValueReadOnly(); const handleEnter: FieldInputEvent = (persistField) => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx index d77131dd3093..f590e5ccb5f1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx @@ -3,19 +3,25 @@ import { useContext } from 'react'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; +import { css } from '@emotion/react'; import { IconListViewGrip } from 'twenty-ui'; -const StyledContainer = styled.div` +const StyledContainer = styled.div<{ isPendingRow?: boolean }>` + border-color: transparent; cursor: grab; - width: 16px; - height: 32px; - z-index: 200; display: flex; - &:hover .icon { - opacity: 1; - } + height: 32px; + width: 16px; + ${({ isPendingRow }) => + !isPendingRow + ? css` + &:hover .icon { + opacity: 1; + } + ` + : ''}; - border-color: transparent; + z-index: 200; `; const StyledIconWrapper = styled.div<{ isDragging: boolean }>` @@ -24,7 +30,9 @@ const StyledIconWrapper = styled.div<{ isDragging: boolean }>` `; export const RecordTableCellGrip = () => { - const { dragHandleProps, isDragging } = useContext(RecordTableRowContext); + const { dragHandleProps, isDragging, isPendingRow } = useContext( + RecordTableRowContext, + ); return ( { hasRightBorder={false} hasBottomBorder={false} > - + diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx index b159c79c0c5e..0a327d097c6f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx @@ -21,7 +21,7 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer'; type RecordTableCellSoftFocusModeProps = { @@ -36,6 +36,8 @@ export const RecordTableCellSoftFocusMode = ({ const { columnIndex } = useContext(RecordTableCellContext); const { recordId } = useContext(FieldContext); + const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow(); + const isFieldReadOnly = useIsFieldValueReadOnly(); const { openTableCell } = useOpenRecordTableCellFromCell(); @@ -135,8 +137,6 @@ export const RecordTableCellSoftFocusMode = ({ */ }; - const { onActionMenuDropdownOpened } = useContext(RecordTableContext); - const handleActionMenuDropdown = (event: React.MouseEvent) => { onActionMenuDropdownOpened(event, recordId); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx new file mode 100644 index 000000000000..146ea602e22d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx @@ -0,0 +1,101 @@ +import { act, renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; +import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; +import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; +import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; +import { + recordTableCell, + recordTableRow, +} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell'; +import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup'; +import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState'; +import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +const setHotkeyScope = jest.fn(); + +jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({ + useSetHotkeyScope: () => setHotkeyScope, +})); + +const onColumnsChange = jest.fn(); +const recordTableId = 'scopeId'; +const recordGroupId = 'recordGroupId'; + +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + { + snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems); + }} + > + + + + + + {children} + + + + + + +); + +describe('useCloseRecordTableCellInGroup', () => { + it('should work as expected', async () => { + const { result } = renderHook( + () => { + const currentTableCellInEditModePosition = useRecoilComponentValueV2( + currentTableCellInEditModePositionComponentState, + ); + const isTableCellInEditMode = useRecoilComponentFamilyValueV2( + isTableCellInEditModeComponentFamilyState, + currentTableCellInEditModePosition, + ); + return { + ...useCloseRecordTableCellInGroup(recordGroupId), + ...useDragSelect(), + isTableCellInEditMode, + }; + }, + { + wrapper: Wrapper, + }, + ); + + act(() => { + result.current.closeTableCellInGroup(); + }); + + expect(result.current.isDragSelectionStartEnabled()).toBe(true); + expect(result.current.isTableCellInEditMode).toBe(false); + expect(setHotkeyScope).toHaveBeenCalledWith('table-soft-focus'); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx similarity index 62% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx index f3ce01a01759..37eda06f10ae 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx @@ -1,22 +1,26 @@ import { act, renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; +import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { recordTableCell, recordTableRow, } from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell'; -import { useCloseRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell'; +import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup'; import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState'; import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const setHotkeyScope = jest.fn(); @@ -28,32 +32,42 @@ const onColumnsChange = jest.fn(); const recordTableId = 'scopeId'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( - + { + snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems); + }} + > - - - - {children} - - - + + + + {children} + + + + ); -describe('useCloseRecordTableCell', () => { +describe('useCloseRecordTableCellNoGroup', () => { it('should work as expected', async () => { const { result } = renderHook( () => { @@ -65,7 +79,7 @@ describe('useCloseRecordTableCell', () => { currentTableCellInEditModePosition, ); return { - ...useCloseRecordTableCell(), + ...useCloseRecordTableCellNoGroup(), ...useDragSelect(), isTableCellInEditMode, }; @@ -76,7 +90,7 @@ describe('useCloseRecordTableCell', () => { ); act(() => { - result.current.closeTableCell(); + result.current.closeTableCellNoGroup(); }); expect(result.current.isDragSelectionStartEnabled()).toBe(true); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts new file mode 100644 index 000000000000..926dade1b55d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts @@ -0,0 +1,56 @@ +import { useRecoilCallback } from 'recoil'; + +import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; + +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode'; +import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; + +export const useCloseRecordTableCellInGroup = (recordGroupId: string) => { + const { recordTableId } = useRecordTableContextOrThrow(); + + const setHotkeyScope = useSetHotkeyScope(); + const { setDragSelectionStartEnabled } = useDragSelect(); + + const { toggleClickOutsideListener } = useClickOutsideListener( + SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID, + ); + + const closeCurrentTableCellInEditMode = + useCloseCurrentTableCellInEditMode(recordTableId); + + const recordTablePendingRecordIdByGroupFamilyState = + useRecoilComponentCallbackStateV2( + recordTablePendingRecordIdByGroupComponentFamilyState, + recordTableId, + ); + + const closeTableCellInGroup = useRecoilCallback( + ({ reset }) => + () => { + toggleClickOutsideListener(true); + setDragSelectionStartEnabled(true); + closeCurrentTableCellInEditMode(); + setHotkeyScope(TableHotkeyScope.TableSoftFocus); + + reset(recordTablePendingRecordIdByGroupFamilyState(recordGroupId)); + }, + [ + closeCurrentTableCellInEditMode, + recordGroupId, + recordTablePendingRecordIdByGroupFamilyState, + setDragSelectionStartEnabled, + setHotkeyScope, + toggleClickOutsideListener, + ], + ); + + return { + closeTableCellInGroup, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup.ts similarity index 66% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2.ts rename to packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup.ts index 204406f88613..f9138eb3f805 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup.ts @@ -5,13 +5,18 @@ import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; +import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; -import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode'; -import { TableHotkeyScope } from '../../types/TableHotkeyScope'; +import { useCallback } from 'react'; + +export const useCloseRecordTableCellNoGroup = () => { + const { recordTableId } = useRecordTableContextOrThrow(); -export const useCloseRecordTableCellV2 = (recordTableId: string) => { const setHotkeyScope = useSetHotkeyScope(); + const { setDragSelectionStartEnabled } = useDragSelect(); const { toggleClickOutsideListener } = useClickOutsideListener( @@ -25,18 +30,25 @@ export const useCloseRecordTableCellV2 = (recordTableId: string) => { recordTablePendingRecordIdComponentState, recordTableId, ); + const resetRecordTablePendingRecordId = useResetRecoilState(pendingRecordIdState); - const closeTableCell = async () => { + const closeTableCellNoGroup = useCallback(() => { toggleClickOutsideListener(true); setDragSelectionStartEnabled(true); closeCurrentTableCellInEditMode(); setHotkeyScope(TableHotkeyScope.TableSoftFocus); resetRecordTablePendingRecordId(); - }; + }, [ + closeCurrentTableCellInEditMode, + resetRecordTablePendingRecordId, + setDragSelectionStartEnabled, + setHotkeyScope, + toggleClickOutsideListener, + ]); return { - closeTableCell, + closeTableCellNoGroup, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts deleted file mode 100644 index 36b32003b7d8..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId'; -import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; - -import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; -import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; -import { useResetRecoilState } from 'recoil'; -import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode'; -import { TableHotkeyScope } from '../../types/TableHotkeyScope'; - -export const useCloseRecordTableCell = () => { - const setHotkeyScope = useSetHotkeyScope(); - const { setDragSelectionStartEnabled } = useDragSelect(); - - const { toggleClickOutsideListener } = useClickOutsideListener( - SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID, - ); - - const closeCurrentTableCellInEditMode = useCloseCurrentTableCellInEditMode(); - const pendingRecordIdState = useRecoilComponentCallbackStateV2( - recordTablePendingRecordIdComponentState, - ); - const resetRecordTablePendingRecordId = - useResetRecoilState(pendingRecordIdState); - - const closeTableCell = async () => { - toggleClickOutsideListener(true); - setDragSelectionStartEnabled(true); - closeCurrentTableCellInEditMode(); - setHotkeyScope(TableHotkeyScope.TableSoftFocus); - resetRecordTablePendingRecordId(); - }; - - return { - closeTableCell, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts index 67583b2f94a1..326947483055 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts @@ -5,12 +5,12 @@ import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useI import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition'; import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; import { TableHotkeyScope } from '../../types/TableHotkeyScope'; export const DEFAULT_CELL_SCOPE: HotkeyScope = { @@ -28,13 +28,16 @@ export type OpenTableCellArgs = { }; export const useOpenRecordTableCellFromCell = () => { - const { onOpenTableCell } = useContext(RecordTableContext); - const cellPosition = useCurrentTableCellPosition(); const customCellHotkeyScope = useContext(CellHotkeyScopeContext); const { recordId, fieldDefinition } = useContext(FieldContext); const { pathToShowPage, objectNameSingular } = useContext( RecordTableRowContext, ); + + const { onOpenTableCell } = useRecordTableBodyContextOrThrow(); + + const cellPosition = useCurrentTableCellPosition(); + const isFieldReadOnly = useIsFieldValueReadOnly(); const openTableCell = ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts index 7e885b4ada80..514bbda1e1e6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts @@ -20,12 +20,11 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { isDefined } from '~/utils/isDefined'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates'; -import { useContext } from 'react'; import { useNavigate } from 'react-router-dom'; import { TableHotkeyScope } from '../../types/TableHotkeyScope'; @@ -49,7 +48,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { const { getClickOutsideListenerIsActivatedState } = useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID); - const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext); + const { indexIdentifierUrl } = useRecordIndexContextOrThrow(); const moveEditModeToTableCellPosition = useMoveEditModeToTableCellPosition(tableScopeId); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index 9c28df97a983..ae08ea612f3c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -1,12 +1,11 @@ import styled from '@emotion/styled'; import { MOBILE_VIEWPORT } from 'twenty-ui'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell'; import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn'; import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn'; import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn'; -import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; const StyledTableHead = styled.thead` cursor: pointer; @@ -77,14 +76,8 @@ const StyledTableHead = styled.thead` } `; -export const RecordTableHeader = ({ - objectNameSingular, -}: { - objectNameSingular: string; -}) => { - const visibleTableColumns = useRecoilComponentValueV2( - visibleTableColumnsComponentSelector, - ); +export const RecordTableHeader = () => { + const { visibleTableColumns } = useRecordTableContextOrThrow(); return ( @@ -92,11 +85,7 @@ export const RecordTableHeader = ({ {visibleTableColumns.map((column) => ( - + ))} 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 5a3e4299a445..2af8387ba78b 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 @@ -3,9 +3,9 @@ import { useCallback, useMemo, useState } from 'react'; import { useRecoilCallback } from 'recoil'; import { IconPlus, LightIconButton } from 'twenty-ui'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; @@ -95,16 +95,14 @@ const StyledHeaderIcon = styled.div` margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)}; `; +type RecordTableHeaderCellProps = { + column: ColumnDefinition; +}; + export const RecordTableHeaderCell = ({ column, - objectNameSingular, -}: { - column: ColumnDefinition; - objectNameSingular: string; -}) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); +}: RecordTableHeaderCellProps) => { + const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow(); const resizeFieldOffsetState = useRecoilComponentCallbackStateV2( resizeFieldOffsetComponentState, @@ -199,7 +197,7 @@ export const RecordTableHeaderCell = ({ const disableColumnResize = column.isLabelIdentifier && isMobile && !isRecordTableScrolledLeft; - const { createNewTableRecord } = useCreateNewTableRecord(); + const { createNewTableRecord } = useCreateNewTableRecord(recordTableId); const handlePlusButtonClick = () => { createNewTableRecord(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index fa7f44137eeb..95db8d71f28b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -1,11 +1,11 @@ -import { useCallback, useContext } from 'react'; +import { useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { IconSettings, MenuItem, UndecoratedLink, useIcons } from 'twenty-ui'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useTableColumns } from '@/object-record/record-table/hooks/useTableColumns'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; @@ -16,7 +16,8 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableHeaderPlusButtonContent = () => { - const { objectMetadataItem } = useContext(RecordTableContext); + const { objectMetadataItem } = useRecordTableContextOrThrow(); + const { closeDropdown } = useDropdown(); const hiddenTableColumns = useRecoilComponentValueV2( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow.tsx new file mode 100644 index 000000000000..553551f002a1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow.tsx @@ -0,0 +1,25 @@ +import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; +import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; +import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; + +export const RecordTablePendingRecordGroupRow = () => { + const currentRecordGroupId = useCurrentRecordGroupId(); + + const pendingRecordId = useRecoilComponentFamilyValueV2( + recordTablePendingRecordIdByGroupComponentFamilyState, + currentRecordGroupId, + ); + + if (!pendingRecordId) return <>; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx index 54e19130074b..6cacd8c0d240 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableRowWrapper.tsx @@ -4,8 +4,8 @@ import { ReactNode, useContext, useEffect, useRef } from 'react'; import { useInView } from 'react-intersection-observer'; import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; @@ -31,8 +31,8 @@ export const RecordTableRowWrapper = ({ }: RecordTableRowWrapperProps) => { const trRef = useRef(null); - const { objectMetadataItem } = useContext(RecordTableContext); - const { onIndexRecordsLoaded } = useContext(RecordIndexRootPropsContext); + const { objectMetadataItem } = useRecordTableContextOrThrow(); + const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow(); const theme = useTheme(); @@ -78,7 +78,12 @@ export const RecordTableRowWrapper = ({ }, [inView, onIndexRecordsLoaded]); return ( - + {(draggableProvided, draggableSnapshot) => ( { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow.tsx new file mode 100644 index 000000000000..8d6125dabca5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow.tsx @@ -0,0 +1,7 @@ +import styled from '@emotion/styled'; + +const StyledTrContainer = styled.tr` + height: 32px; +`; + +export const RecordTableRecordGroupEmptyRow = StyledTrContainer; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx new file mode 100644 index 000000000000..7217f372658d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx @@ -0,0 +1,36 @@ +import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; +import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow'; +import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { IconPlus } from 'twenty-ui'; +import { isDefined } from '~/utils/isDefined'; + +export const RecordTableRecordGroupSectionAddNew = () => { + const { recordTableId } = useRecordTableContextOrThrow(); + + const currentRecordGroupId = useCurrentRecordGroupId(); + + const pendingRecordId = useRecoilComponentFamilyValueV2( + recordTablePendingRecordIdByGroupComponentFamilyState, + currentRecordGroupId, + ); + + const { createNewTableRecordInGroup } = + useCreateNewTableRecord(recordTableId); + + const handleAddNewRecord = () => { + createNewTableRecordInGroup(currentRecordGroupId); + }; + + if (isDefined(pendingRecordId)) return null; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore.tsx index 0418a8a4646f..244de469bce2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore.tsx @@ -1,14 +1,13 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable'; import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; -import { useContext } from 'react'; import { IconArrowDown } from 'twenty-ui'; export const RecordTableRecordGroupSectionLoadMore = () => { - const { objectNameSingular } = useContext(RecordTableContext); + const { objectNameSingular } = useRecordTableContextOrThrow(); const currentRecordGroupId = useCurrentRecordGroupId(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState.ts new file mode 100644 index 000000000000..9eaa9c938235 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState.ts @@ -0,0 +1,10 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; + +export const recordTablePendingRecordIdByGroupComponentFamilyState = + createComponentFamilyStateV2({ + key: 'recordTablePendingRecordIdByGroupComponentFamilyState', + defaultValue: null, + componentInstanceContext: RecordTableComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts index 4923567c34dc..3c84a42a2268 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdComponentState.ts @@ -4,7 +4,7 @@ import { createComponentStateV2 } from '@/ui/utilities/state/component-state/uti export const recordTablePendingRecordIdComponentState = createComponentStateV2< string | null >({ - key: 'recordTablePendingRecordIdState', + key: 'recordTablePendingRecordIdComponentState', defaultValue: null, componentInstanceContext: RecordTableComponentInstanceContext, }); 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 ae4a08c67d15..3a86e532850e 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 @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { ViewBar } from '@/views/components/ViewBar'; @@ -28,7 +28,7 @@ export const SignInBackgroundMockContainer = () => { return ( - { objectMetadataItem, onIndexRecordsLoaded: () => {}, indexIdentifierUrl: () => '', - onCreateRecord: () => {}, }} > { - + ); }; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index b159ffd51c9d..5d8e87d1f6a2 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -1,6 +1,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords'; @@ -18,7 +18,6 @@ import { GraphQLView } from '@/views/types/GraphQLView'; import { View } from '@/views/types/View'; import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewType } from '@/views/types/ViewType'; -import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-ui'; import { v4 } from 'uuid'; @@ -57,7 +56,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const { createViewFilterGroupRecords } = usePersistViewFilterGroupRecords(); - const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); + const { objectMetadataItem } = useRecordIndexContextOrThrow(); const createViewFromCurrentView = useRecoilCallback( ({ snapshot, set }) => diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 0f367b73de27..41d3a2a7264d 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -12,9 +12,8 @@ import { RecordIndexContainer } from '@/object-record/record-index/components/Re import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect'; import { RecordIndexContainerContextStoreObjectMetadataEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect'; import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader'; -import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick'; -import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; import { PageBody } from '@/ui/layout/page/components/PageBody'; import { PageContainer } from '@/ui/layout/page/components/PageContainer'; import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; @@ -41,12 +40,6 @@ export const RecordIndexPage = () => { objectNameSingular, }); - const { createNewTableRecord } = useCreateNewTableRecord(recordIndexId); - - const handleCreateRecord = () => { - createNewTableRecord(); - }; - const { indexIdentifierUrl } = useHandleIndexIdentifierClick({ objectMetadataItem, recordIndexId, @@ -63,7 +56,7 @@ export const RecordIndexPage = () => { return ( - { objectMetadataItem, onIndexRecordsLoaded: handleIndexRecordsLoaded, indexIdentifierUrl, - onCreateRecord: handleCreateRecord, }} > { - + ); }; diff --git a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx index ba9c9dfeb224..d7e05bd05afd 100644 --- a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx @@ -2,7 +2,8 @@ import { Decorator } from '@storybook/react'; import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext'; +import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext'; import { isDefined } from 'twenty-ui'; export const RecordTableDecorator: Decorator = (Story) => { @@ -17,23 +18,28 @@ export const RecordTableDecorator: Decorator = (Story) => { } return ( - {}, - onCloseTableCell: () => {}, - onOpenTableCell: () => {}, - onActionMenuDropdownOpened: () => {}, - onMoveFocus: () => {}, - onMoveSoftFocusToCell: () => {}, - onUpsertRecord: () => {}, recordTableId: 'persons', viewBarId: 'view-bar', visibleTableColumns: [], }} > - - + {}, + onCloseTableCell: () => {}, + onOpenTableCell: () => {}, + onActionMenuDropdownOpened: () => {}, + onMoveFocus: () => {}, + onMoveSoftFocusToCell: () => {}, + onUpsertRecord: () => {}, + }} + > + + + ); }; diff --git a/packages/twenty-front/src/utils/createRequiredContext.ts b/packages/twenty-front/src/utils/createRequiredContext.ts new file mode 100644 index 000000000000..7e0872d8549b --- /dev/null +++ b/packages/twenty-front/src/utils/createRequiredContext.ts @@ -0,0 +1,20 @@ +import React, { useContext } from 'react'; + +export const createRequiredContext = (debugName: string) => { + const Context = React.createContext(undefined); + Context.displayName = `${debugName}Provider`; + + const useRequiredContextOrThrow = (): TContext => { + const context = useContext(Context); + + if (context === undefined) { + throw new Error( + `${debugName} Context not found. Please wrap your component tree with <${Context.displayName}> before using use${debugName}OrThrow().`, + ); + } + + return context; + }; + + return [Context.Provider, useRequiredContextOrThrow] as const; +}; diff --git a/packages/twenty-front/src/utils/createRootPropsContext.ts b/packages/twenty-front/src/utils/createRootPropsContext.ts deleted file mode 100644 index 9ace2a67ede2..000000000000 --- a/packages/twenty-front/src/utils/createRootPropsContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Context, createContext } from 'react'; - -type RootProps = Record; - -export type RootPropsContext = T extends RootProps - ? T - : never; - -export const createRootPropsContext = (): Context< - RootPropsContext -> => createContext>({} as RootPropsContext);