From e7ed9530ca32584dda44fd96683abee630b86e70 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 30 Jan 2025 17:48:38 +0100 Subject: [PATCH] Implement parallel code path for fieldMetadataItem instead of filterDefinition (#9931) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR doesn't remove or change the current behavior of the filter definition used in filter dropdown, but adds a parallel code path where we set the field metadata item used in filter dropdown, which is enough to replace the filter definition. The goal at the end is to compute dynamically the equivalent of filter definition where needed, by deriving from objectMetadataItems global state + fieldMetadataItemId used in dropdown, that way we don't create any other source of truth for the concept of filter definition and everything is easier to work with, especially with advanced filters. The general spirit is that it's always better to derive everywhere from a unique state as much as possible, and only create the equivalent of selectors where needed that will only take the relevant chunk of state for the small zone of the code operating some reading/writing. - Added utils and hooks to get a FieldMetadataItem more easily - Removed some properties from RecordFilterDefinition (the easiest to remove) and replaced them with a dynamic logic, deriving what's needed where it is needed - Added a new fieldMetadataItemIdUsedInDropdownComponentState that is set in parallel of filterDefinitionUsedInDropdown (to prepare the removal of filter definition used in dropdown) - Fixed some stories --------- Co-authored-by: Raphaƫl Bosi <71827178+bosiraphael@users.noreply.github.com> --- .../__stories__/PrefetchLoading.stories.tsx | 2 +- .../hooks/useFieldMetadataItemById.ts | 17 ++++ .../hooks/useGetFieldMetadataItemById.ts | 23 ++++++ ...atFieldMetadataItemsAsFilterDefinitions.ts | 20 ++++- .../AdvancedFilterViewFilterValueInput.tsx | 6 ++ .../ObjectFilterDropdownFilterSelect.tsx | 13 ++- ...pdownFilterSelectCompositeFieldSubMenu.tsx | 7 ++ ...jectFilterDropdownFilterSelectMenuItem.tsx | 13 ++- .../ObjectFilterDropdownRecordSelect.tsx | 17 +++- ...SingleEntityObjectFilterDropdownButton.tsx | 19 ++--- .../MultipleFiltersDropdownButton.stories.tsx | 81 ++++++------------- ...useSelectFilterDefinitionUsedInDropdown.ts | 7 ++ ...adataItemIdUsedInDropdownComponentState.ts | 9 +++ ...dataItemUsedInDropdownComponentSelector.ts | 31 +++++++ ...ubFieldNameUsedInDropdownComponentState.ts | 10 +++ .../RelationToOneFieldInput.stories.tsx | 5 +- .../record-filter/types/RecordFilter.ts | 2 + .../types/RecordFilterDefinition.ts | 6 -- .../hooks/useHandleToggleColumnFilter.ts | 11 ++- .../SingleRecordSelectMenuItems.tsx | 43 +--------- .../EditableFilterDropdownButton.tsx | 7 ++ 21 files changed, 221 insertions(+), 128 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts diff --git a/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx b/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx index 3e84058d3..0f94fc9a0 100644 --- a/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx +++ b/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx @@ -36,6 +36,6 @@ export const Default: Story = { await canvas.findByText('Search'); await canvas.findByText('Settings'); await canvas.findByText('Linkedin'); - await canvas.findByText('All'); + await canvas.findByText('All companies'); }, }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts new file mode 100644 index 000000000..1daffe5f9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItemById.ts @@ -0,0 +1,17 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useFieldMetadataItemById = (fieldMetadataId: string) => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const fieldMetadataItem = objectMetadataItems + .flatMap((objectMetadataItem) => objectMetadataItem.fields) + .find((field) => field.id === fieldMetadataId); + + if (!isDefined(fieldMetadataItem)) { + throw new Error(`Field metadata item not found for id ${fieldMetadataId}`); + } + + return { fieldMetadataItem }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts new file mode 100644 index 000000000..c1827d284 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useGetFieldMetadataItemById.ts @@ -0,0 +1,23 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useGetFieldMetadataItemById = () => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const getFieldMetadataItemById = (fieldMetadataId: string) => { + const fieldMetadataItem = objectMetadataItems + .flatMap((objectMetadataItem) => objectMetadataItem.fields) + .find((field) => field.id === fieldMetadataId); + + if (!isDefined(fieldMetadataItem)) { + throw new Error( + `Field metadata item not found for id ${fieldMetadataId}`, + ); + } + + return fieldMetadataItem; + }; + + return { getFieldMetadataItemById }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts index b817bdf53..35eaa9130 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts @@ -60,13 +60,25 @@ export const formatFieldMetadataItemAsFilterDefinition = ({ fieldMetadataId: field.id, label: field.label, iconName: field.icon ?? 'Icon123', - relationObjectMetadataNamePlural: - field.relationDefinition?.targetObjectMetadata.namePlural, - relationObjectMetadataNameSingular: - field.relationDefinition?.targetObjectMetadata.nameSingular, type: getFilterTypeFromFieldType(field.type), }); +export const getRelationObjectMetadataNameSingular = ({ + field, +}: { + field: ObjectMetadataItem['fields'][0]; +}): string | undefined => { + return field.relationDefinition?.targetObjectMetadata.nameSingular; +}; + +export const getRelationObjectMetadataNamePlural = ({ + field, +}: { + field: ObjectMetadataItem['fields'][0]; +}): string | undefined => { + return field.relationDefinition?.targetObjectMetadata.namePlural; +}; + export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => { switch (fieldType) { case FieldMetadataType.DATE_TIME: diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx index e40191418..48c8c0a7b 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx @@ -1,5 +1,6 @@ import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter'; import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; @@ -26,6 +27,10 @@ export const AdvancedFilterViewFilterValueInput = ({ filterDefinitionUsedInDropdownComponentState, ); + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( selectedOperandInDropdownComponentState, ); @@ -58,6 +63,7 @@ export const AdvancedFilterViewFilterValueInput = ({ /> } onOpen={() => { + setFieldMetadataItemIdUsedInDropdown(filter.fieldMetadataId); setFilterDefinitionUsedInDropdown(filter.definition); setSelectedOperandInDropdown(filter.operand); setSelectedFilter(filter); 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 69a87b395..efcc83ba5 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 @@ -25,6 +25,7 @@ import { isDefined } from 'twenty-ui'; import { FeatureFlagKey } from '~/generated/graphql'; import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { useLingui } from '@lingui/react/macro'; @@ -126,11 +127,15 @@ export const ObjectFilterDropdownFilterSelect = ({ const { selectFilterDefinitionUsedInDropdown } = useSelectFilterDefinitionUsedInDropdown(); + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + const { resetSelectedItem } = useSelectableList(OBJECT_FILTER_DROPDOWN_ID); - const handleEnter = (itemId: string) => { + const handleEnter = (fieldMetadataItemId: string) => { const selectedFilterDefinition = availableFilterDefinitions.find( - (item) => item.fieldMetadataId === itemId, + (item) => item.fieldMetadataId === fieldMetadataItemId, ); if (!isDefined(selectedFilterDefinition)) { @@ -143,6 +148,10 @@ export const ObjectFilterDropdownFilterSelect = ({ filterDefinition: selectedFilterDefinition, }); + setFieldMetadataItemIdUsedInDropdown( + selectedFilterDefinition.fieldMetadataId, + ); + closeAdvancedFilterDropdown(); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx index 5f1fa512d..71b3b356c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx @@ -1,6 +1,7 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown'; import { advancedFilterViewFilterGroupIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState'; import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState'; @@ -61,6 +62,10 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { filterDefinitionUsedInDropdownComponentState, ); + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( selectedOperandInDropdownComponentState, ); @@ -110,6 +115,7 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { } setFilterDefinitionUsedInDropdown(definition); + setFieldMetadataItemIdUsedInDropdown(definition.fieldMetadataId); setSelectedOperandInDropdown( getRecordFilterOperandsForRecordFilterDefinition(definition)[0], @@ -122,6 +128,7 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { }; const handleSubMenuBack = () => { + setFieldMetadataItemIdUsedInDropdown(null); setFilterDefinitionUsedInDropdown(null); setObjectFilterDropdownSubMenuFieldType(null); setObjectFilterDropdownFirstLevelFilterDefinition(null); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index 6e5ef654d..fcccd4b6e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -1,15 +1,16 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; + import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; -import { CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType'; - import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType'; import { RecordFilterDefinition } from '@/object-record/record-filter/types/RecordFilterDefinition'; import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; @@ -31,6 +32,10 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ const { selectFilterDefinitionUsedInDropdown } = useSelectFilterDefinitionUsedInDropdown(); + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + const [, setObjectFilterDropdownFirstLevelFilterDefinition] = useRecoilComponentStateV2( objectFilterDropdownFirstLevelFilterDefinitionComponentState, @@ -82,6 +87,10 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ filterDefinition: availableFilterDefinition, }); + setFieldMetadataItemIdUsedInDropdown( + availableFilterDefinition.fieldMetadataId, + ); + if ( availableFilterDefinition.type === 'RELATION' || availableFilterDefinition.type === 'SELECT' diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index d8cdad553..b63a8817e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -2,8 +2,10 @@ import { useState } from 'react'; import { v4 } from 'uuid'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { getRelationObjectMetadataNameSingular } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { ObjectFilterDropdownRecordPinnedItems } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems'; import { CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID } from '@/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId'; +import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; @@ -41,6 +43,10 @@ export const ObjectFilterDropdownRecordSelect = ({ filterDefinitionUsedInDropdownComponentState, ); + const fieldMetadataItemUsedInFilterDropdown = useRecoilComponentValueV2( + fieldMetadataItemUsedInDropdownComponentSelector, + ); + const selectedOperandInDropdown = useRecoilComponentValueV2( selectedOperandInDropdownComponentState, ); @@ -73,15 +79,20 @@ export const ObjectFilterDropdownRecordSelect = ({ }) .parse(selectedFilter?.value); - const objectNameSingular = - filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular; + if (!isDefined(fieldMetadataItemUsedInFilterDropdown)) { + throw new Error('fieldMetadataItemUsedInFilterDropdown is not defined'); + } + + const objectNameSingular = getRelationObjectMetadataNameSingular({ + field: fieldMetadataItemUsedInFilterDropdown, + }); if (!isDefined(objectNameSingular)) { throw new Error('relationObjectMetadataNameSingular is not defined'); } const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: objectNameSingular, + objectNameSingular, }); const objectLabelPlural = objectMetadataItem?.labelPlural; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx index 20b57d9b0..4be9678ab 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx @@ -7,8 +7,8 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; @@ -36,6 +36,10 @@ export const SingleEntityObjectFilterDropdownButton = ({ filterDefinitionUsedInDropdownComponentState, ); + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + ); + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( selectedOperandInDropdownComponentState, ); @@ -47,6 +51,9 @@ export const SingleEntityObjectFilterDropdownButton = ({ const availableFilterDefinition = availableFilterDefinitions[0]; React.useEffect(() => { + setFieldMetadataItemIdUsedInDropdown( + availableFilterDefinition.fieldMetadataId, + ); setFilterDefinitionUsedInDropdown(availableFilterDefinition); const defaultOperand = getRecordFilterOperandsForRecordFilterDefinition( availableFilterDefinition, @@ -56,6 +63,7 @@ export const SingleEntityObjectFilterDropdownButton = ({ availableFilterDefinition, setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, + setFieldMetadataItemIdUsedInDropdown, ]); const theme = useTheme(); @@ -69,14 +77,7 @@ export const SingleEntityObjectFilterDropdownButton = ({ clickableComponent={ {selectedFilter ? ( - + ) : ( t`Filter` )} 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 b209520ec..ad980c620 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 @@ -3,13 +3,14 @@ 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 { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; +import { formatFieldMetadataItemAsFilterDefinition } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; 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'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; @@ -18,7 +19,6 @@ import { ComponentDecorator, getCanvasElementForDropdownTesting, } from 'twenty-ui'; -import { FieldMetadataType } from '~/generated/graphql'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; @@ -45,59 +45,26 @@ const meta: Meta = { instanceId, ); - setTableColumns([ - { - fieldMetadataId: '1', - iconName: 'IconUser', - label: 'Text', - type: FieldMetadataType.TEXT, - isVisible: true, - metadata: { - fieldName: 'text', - }, - } as ColumnDefinition, - { - fieldMetadataId: '3', - iconName: 'IconNumber', - label: 'Number', - type: FieldMetadataType.NUMBER, - isVisible: true, - metadata: { - fieldName: 'number', - }, - } as ColumnDefinition, - { - fieldMetadataId: '4', - iconName: 'IconCalendar', - label: 'Date', - type: FieldMetadataType.DATE_TIME, - isVisible: true, - metadata: { - fieldName: 'date', - }, - } as ColumnDefinition, - ]); + const columns = companyObjectMetadataItem.fields.map( + (fieldMetadataItem, index) => + formatFieldMetadataItemAsColumnDefinition({ + field: fieldMetadataItem, + objectMetadataItem: companyObjectMetadataItem, + position: index, + }), + ); + + const filterDefinitions = companyObjectMetadataItem.fields.map( + (fieldMetadataItem) => + formatFieldMetadataItemAsFilterDefinition({ + field: fieldMetadataItem, + }), + ); + + setTableColumns(columns); + + setAvailableFilterDefinitions(filterDefinitions); - setAvailableFilterDefinitions([ - { - fieldMetadataId: '1', - iconName: 'IconUser', - label: 'Text', - type: FieldMetadataType.TEXT, - }, - { - fieldMetadataId: '3', - iconName: 'IconNumber', - label: 'Number', - type: FieldMetadataType.NUMBER, - }, - { - fieldMetadataId: '3', - iconName: 'IconCalendar', - label: 'Date', - type: FieldMetadataType.DATE_TIME, - }, - ]); return ( { setFilterDefinitionUsedInDropdown(filterDefinition); + setFieldMetadataItemIdUsedInDropdown(filterDefinition.fieldMetadataId); if ( filterDefinition.type === 'RELATION' || diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState.ts new file mode 100644 index 000000000..cefeb2b73 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState.ts @@ -0,0 +1,9 @@ +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const fieldMetadataItemIdUsedInDropdownComponentState = + createComponentStateV2({ + key: 'fieldMetadataItemIdUsedInDropdownComponentState', + defaultValue: null, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector.ts new file mode 100644 index 000000000..bf3e6622e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector.ts @@ -0,0 +1,31 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; + +export const fieldMetadataItemUsedInDropdownComponentSelector = + createComponentSelectorV2({ + key: 'fieldMetadataItemUsedInDropdownComponentSelector', + get: + ({ instanceId }) => + ({ get }) => { + const fieldMetadataItemIdUsedInDropdown = get( + fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({ + instanceId, + }), + ); + + const objectMetadataItems = get(objectMetadataItemsState); + + const correspondingFieldMetadataItem = objectMetadataItems + .flatMap((objectMetadataItem) => objectMetadataItem.fields) + .find( + (fieldMetadataItem) => + fieldMetadataItem.id === fieldMetadataItemIdUsedInDropdown, + ); + + return correspondingFieldMetadataItem; + }, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts new file mode 100644 index 000000000..db286d739 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState.ts @@ -0,0 +1,10 @@ +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const subFieldNameUsedInDropdownComponentState = createComponentStateV2< + string | null +>({ + key: 'subFieldNameUsedInDropdownComponentState', + defaultValue: null, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index 262d00fca..ce70f5d51 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -140,7 +140,10 @@ export const Submit: Story = { }); await userEvent.click(item); - await waitFor(() => expect(submitJestFn).toHaveBeenCalledTimes(1)); + + await waitFor(() => { + expect(submitJestFn).toHaveBeenCalledTimes(1); + }); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts index b993ea318..d6760f92f 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilter.ts @@ -12,4 +12,6 @@ export type RecordFilter = { operand: ViewFilterOperand; positionInViewFilterGroup?: number | null; definition: RecordFilterDefinition; + label?: string; + subFieldName?: string; }; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilterDefinition.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilterDefinition.ts index d549f1038..820195f68 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilterDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/types/RecordFilterDefinition.ts @@ -1,5 +1,3 @@ -import { IconComponent } from 'twenty-ui'; - import { FilterableFieldType } from './FilterableFieldType'; export type RecordFilterDefinition = { @@ -7,9 +5,5 @@ export type RecordFilterDefinition = { label: string; iconName: string; type: FilterableFieldType; - relationObjectMetadataNamePlural?: string; - relationObjectMetadataNameSingular?: string; - selectAllLabel?: string; - SelectAllIcon?: IconComponent; compositeFieldName?: string; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts index daa0bb001..bcc7d25be 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts @@ -3,13 +3,15 @@ import { v4 } from 'uuid'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; +import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; +import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState'; import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { getRecordFilterOperandsForRecordFilterDefinition } from '@/object-record/record-filter/utils/getRecordFilterOperandsForRecordFilterDefinition'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; @@ -56,6 +58,11 @@ export const useHandleToggleColumnFilter = ({ const { selectFilterDefinitionUsedInDropdown } = useSelectFilterDefinitionUsedInDropdown(viewBarId); + const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2( + fieldMetadataItemIdUsedInDropdownComponentState, + viewBarId, + ); + const handleToggleColumnFilter = useCallback( async (fieldMetadataId: string) => { const correspondingColumnDefinition = columnDefinitions.find( @@ -100,6 +107,7 @@ export const useHandleToggleColumnFilter = ({ await upsertCombinedViewFilter(newFilter); selectFilterDefinitionUsedInDropdown({ filterDefinition }); + setFieldMetadataItemIdUsedInDropdown(fieldMetadataId); } openDropdown(existingViewFilter?.id ?? newFilterId); @@ -112,6 +120,7 @@ export const useHandleToggleColumnFilter = ({ currentViewWithCombinedFiltersAndSorts, availableFilterDefinitions, upsertRecordFilter, + setFieldMetadataItemIdUsedInDropdown, ], ); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItems.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItems.tsx index c657ba9a9..77782e550 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItems.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleRecordSelectMenuItems.tsx @@ -24,11 +24,6 @@ export type SingleRecordSelectMenuItemsProps = { onCancel?: () => void; onRecordSelected: (entity?: RecordForSelect) => void; selectedRecord?: RecordForSelect; - SelectAllIcon?: IconComponent; - selectAllLabel?: string; - isAllRecordsSelected?: boolean; - isAllRecordsSelectShown?: boolean; - onAllRecordsSelected?: () => void; hotkeyScope?: string; isFiltered: boolean; shouldSelectEmptyOption?: boolean; @@ -42,11 +37,6 @@ export const SingleRecordSelectMenuItems = ({ onCancel, onRecordSelected, selectedRecord, - SelectAllIcon, - selectAllLabel, - isAllRecordsSelected, - isAllRecordsSelectShown, - onAllRecordsSelected, hotkeyScope = RelationPickerHotkeyScope.RelationPicker, isFiltered, shouldSelectEmptyOption, @@ -61,16 +51,7 @@ export const SingleRecordSelectMenuItems = ({ } : null; - const selectAll = isAllRecordsSelectShown - ? { - __typename: '', - id: 'select-all', - name: selectAllLabel, - } - : null; - const recordsInDropdown = [ - selectAll, selectNone, selectedRecord, ...recordsToSelect, @@ -87,10 +68,6 @@ export const SingleRecordSelectMenuItems = ({ isSelectedItemIdSelector('select-none'), ); - const isSelectedSelectAllButton = useRecoilValue( - isSelectedItemIdSelector('select-all'), - ); - useScopedHotkeys( [Key.Escape], () => { @@ -120,9 +97,7 @@ export const SingleRecordSelectMenuItems = ({ {loading && !isFiltered ? ( - ) : recordsInDropdown.length === 0 && - !isAllRecordsSelectShown && - !loading ? ( + ) : recordsInDropdown.length === 0 && !loading ? ( <> ) : ( recordsInDropdown?.map((record) => { @@ -141,22 +116,6 @@ export const SingleRecordSelectMenuItems = ({ ) ); } - case 'select-all': { - return ( - isAllRecordsSelectShown && - selectAllLabel && - onAllRecordsSelected && ( - onAllRecordsSelected()} - LeftIcon={SelectAllIcon} - text={selectAllLabel} - selected={!!isAllRecordsSelected} - hovered={isSelectedSelectAllButton} - /> - ) - ); - } default: { return (