From 4b3614b413e6b5cad141932d0aa47b3129ace8d4 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 4 Aug 2026 13:11:53 +0200 Subject: [PATCH] fix(front): show relation value chip (Me / record names) in advanced filters (#23718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem In advanced filters, a relation filter on a workspace-member field (e.g. Assignee "is Me") displayed its raw JSON value `{"isCurrentWorkspaceMemberSelected":true,...}` instead of a readable chip. Regular (non-advanced) filters handle this correctly: `EditableRelationFilterChip` computes the label at runtime via `useComputeRecordRelationFilterLabelValue`, rendering "Me", the selected record names, or "N members". The advanced filter value input instead relied on the deprecated stored `displayValue` through `getRecordFilterDisplayValue`, which has no `RELATION` branch and falls back to the raw value. When a saved view filter carries no `displayValue` (it defaults to the raw stringified value in `mapViewFiltersToFilters`), the raw JSON leaked into the UI. ## Fix - Extract the relation value-label computation into a shared hook `useComputeRecordRelationFilterDisplayValue` (parses the relation value, resolves "Me" + record names). - `useComputeRecordRelationFilterLabelValue` now consumes it (regular chips unchanged). - The advanced filter clickable select renders a dedicated `AdvancedFilterRelationValueInputClickableSelect` for `RELATION` filters, computing the label at runtime just like regular filters. ## Proof Both filter surfaces render the relation value as **Me**, not the raw `{"isCurrentWorkspaceMemberSelected":...}` JSON. The advanced-filter shot loads a **saved view in a fresh session** — the exact bug condition, where the view filter carries no stored `displayValue`. **Regular filter chip** image **Advanced filter** image ## Test Verified manually on a local instance: created a saved view with an advanced filter `Account Owner Is Me`, then reloaded it in a fresh session — the condition where the view filter carries no stored `displayValue`. The value renders as "Me" instead of the raw JSON. Review in cubic --- ...ilterRelationValueInputClickableSelect.tsx | 23 +++++ ...alueInputDropdownButtonClickableSelect.tsx | 13 +++ ...omputeRecordRelationFilterDisplayValue.tsx | 96 ++++++++++++++++++ ...eComputeRecordRelationFilterLabelValue.tsx | 98 +++---------------- 4 files changed, 146 insertions(+), 84 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationValueInputClickableSelect.tsx create mode 100644 packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterDisplayValue.tsx diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationValueInputClickableSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationValueInputClickableSelect.tsx new file mode 100644 index 0000000000..61fcf935cc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterRelationValueInputClickableSelect.tsx @@ -0,0 +1,23 @@ +import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { SelectControl } from '@/ui/input/components/SelectControl'; +import { useComputeRecordRelationFilterDisplayValue } from '@/views/hooks/useComputeRecordRelationFilterDisplayValue'; + +type AdvancedFilterRelationValueInputClickableSelectProps = { + recordFilter: RecordFilter; +}; + +export const AdvancedFilterRelationValueInputClickableSelect = ({ + recordFilter, +}: AdvancedFilterRelationValueInputClickableSelectProps) => { + const { displayValue } = useComputeRecordRelationFilterDisplayValue({ + recordFilter, + }); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx index cdb62c0635..f65735041a 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterValueInputDropdownButtonClickableSelect.tsx @@ -1,3 +1,4 @@ +import { AdvancedFilterRelationValueInputClickableSelect } from '@/object-record/advanced-filter/components/AdvancedFilterRelationValueInputClickableSelect'; import { getAdvancedFilterInputPlaceholderText } from '@/object-record/advanced-filter/utils/getAdvancedFilterInputPlacedholderText'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { SelectControl } from '@/ui/input/components/SelectControl'; @@ -70,6 +71,18 @@ export const AdvancedFilterValueInputDropdownButtonClickableSelect = ({ const isDateTimeType = recordFilter?.type === 'DATE' || recordFilter?.type === 'DATE_TIME'; + if ( + recordFilter?.type === 'RELATION' && + !shouldUsePlaceholder && + isDefined(recordFilter.fieldMetadataId) + ) { + return ( + + ); + } + return isDateTimeType ? ( {advancedFilterInputText} ) : ( diff --git a/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterDisplayValue.tsx b/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterDisplayValue.tsx new file mode 100644 index 0000000000..2cc1ec4a8a --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterDisplayValue.tsx @@ -0,0 +1,96 @@ +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getRelationObjectMetadataNameSingular } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; +import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFieldMetadataItemByIdOrThrow'; +import { MAX_RECORDS_TO_DISPLAY } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; +import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; +import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; + +import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { t } from '@lingui/core/macro'; +import { + arrayOfUuidOrVariableSchema, + isDefined, + jsonRelationFilterValueSchema, +} from 'twenty-shared/utils'; + +type UseComputeRecordRelationFilterDisplayValueParams = { + recordFilter: RecordFilter; +}; + +// The stored displayValue is deprecated and empty for filters created without one, so compute the label at runtime. +export const useComputeRecordRelationFilterDisplayValue = ({ + recordFilter, +}: UseComputeRecordRelationFilterDisplayValueParams) => { + const allowRequestsToTwentyIcons = useAtomStateValue( + allowRequestsToTwentyIconsState, + ); + + const { objectMetadataItems } = useObjectMetadataItems(); + + if (!isDefined(recordFilter.fieldMetadataId)) { + throw new Error('recordFilter.fieldMetadataId is not defined'); + } + + // Nested relation filters resolve records from the leaf relation's target object, direct filters from the source. + const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow({ + fieldMetadataId: + recordFilter.relationTargetFieldMetadataId ?? + recordFilter.fieldMetadataId, + objectMetadataItems, + }); + + const relationObjectMetadataNameSingular = + getRelationObjectMetadataNameSingular({ + field: fieldMetadataItem, + }); + + if (!isDefined(relationObjectMetadataNameSingular)) { + throw new Error('relationObjectMetadataNameSingular is not defined'); + } + + const relationObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === relationObjectMetadataNameSingular, + ); + + if (!isDefined(relationObjectMetadataItem)) { + throw new Error('relationObjectMetadataItem is not defined'); + } + + const relationObjectLabelPlural = relationObjectMetadataItem.labelPlural; + + const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = + jsonRelationFilterValueSchema + .catch({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: arrayOfUuidOrVariableSchema.parse( + recordFilter.value, + ), + }) + .parse(recordFilter.value); + + const { selectedRecords, loading } = useRecordsForSelect({ + searchFilterText: '', + selectedIds: selectedRecordIds, + objectNameSingular: relationObjectMetadataNameSingular, + limit: 10, + allowRequestsToTwentyIcons, + }); + + if (loading) { + return { displayValue: t`Loading...`, loading }; + } + + const labelValueItems = [ + ...(isCurrentWorkspaceMemberSelected ? [t`Me`] : []), + ...selectedRecords.map((record) => record.name), + ]; + + const displayValue = + labelValueItems.length > MAX_RECORDS_TO_DISPLAY + ? `${labelValueItems.length} ${relationObjectLabelPlural.toLowerCase()}` + : labelValueItems.join(', '); + + return { displayValue, loading }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx b/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx index eee38bff62..f25b383f59 100644 --- a/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx +++ b/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx @@ -1,19 +1,9 @@ -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { getRelationObjectMetadataNameSingular } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; -import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFieldMetadataItemByIdOrThrow'; -import { MAX_RECORDS_TO_DISPLAY } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; -import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; +import { useComputeRecordRelationFilterDisplayValue } from '@/views/hooks/useComputeRecordRelationFilterDisplayValue'; import { useGetRecordFilterChipLabelValue } from '@/views/hooks/useGetRecordFilterChipLabelValue'; +import { isNonEmptyString } from '@sniptt/guards'; import { t } from '@lingui/core/macro'; -import { - arrayOfUuidOrVariableSchema, - isDefined, - jsonRelationFilterValueSchema, -} from 'twenty-shared/utils'; -import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; type ObjectFilterDropdownRecordSelectProps = { recordFilter: RecordFilter; @@ -23,86 +13,26 @@ type ObjectFilterDropdownRecordSelectProps = { export const useComputeRecordRelationFilterLabelValue = ({ recordFilter, }: ObjectFilterDropdownRecordSelectProps) => { - const allowRequestsToTwentyIcons = useAtomStateValue( - allowRequestsToTwentyIconsState, - ); - - const { objectMetadataItems } = useObjectMetadataItems(); - const { getRecordFilterChipLabelValue } = useGetRecordFilterChipLabelValue(); - if (!isDefined(recordFilter.fieldMetadataId)) { - throw new Error('fieldMetadataItemUsedInFilterDropdown is not defined'); - } - - const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow({ - fieldMetadataId: recordFilter.fieldMetadataId, - objectMetadataItems, - }); - - const relationObjectMetadataNameSingular = - getRelationObjectMetadataNameSingular({ - field: fieldMetadataItem, - }); - - if (!isDefined(relationObjectMetadataNameSingular)) { - throw new Error('relationObjectMetadataNameSingular is not defined'); - } - - const relationObjectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === relationObjectMetadataNameSingular, - ); - - if (!isDefined(relationObjectMetadataItem)) { - throw new Error('relationObjectMetadataItem is not defined'); - } - - const relationObjectLabelPlural = relationObjectMetadataItem.labelPlural; - - const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = - jsonRelationFilterValueSchema - .catch({ - isCurrentWorkspaceMemberSelected: false, - selectedRecordIds: arrayOfUuidOrVariableSchema.parse( - recordFilter.value, - ), - }) - .parse(recordFilter.value); - - const { selectedRecords, loading } = useRecordsForSelect({ - searchFilterText: '', - selectedIds: selectedRecordIds, - objectNameSingular: relationObjectMetadataNameSingular, - limit: 10, - allowRequestsToTwentyIcons, + const { displayValue, loading } = useComputeRecordRelationFilterDisplayValue({ + recordFilter, }); if (loading) { return { labelValue: t`: Loading...` }; } - const labelValueItems = [ - ...(isCurrentWorkspaceMemberSelected ? [t`Me`] : []), - ...selectedRecords.map((record) => record.name), - ]; - - const filterDisplayValue = - labelValueItems.length > MAX_RECORDS_TO_DISPLAY - ? `${labelValueItems.length} ${relationObjectLabelPlural.toLowerCase()}` - : labelValueItems.join(', '); - return { - labelValue: - labelValueItems.length > 0 - ? getRecordFilterChipLabelValue({ - recordFilter: { - ...recordFilter, - displayValue: filterDisplayValue, - }, - }) - : getRecordFilterChipLabelValue({ - recordFilter, - }), + labelValue: isNonEmptyString(displayValue) + ? getRecordFilterChipLabelValue({ + recordFilter: { + ...recordFilter, + displayValue, + }, + }) + : getRecordFilterChipLabelValue({ + recordFilter, + }), }; };