fix(front): show relation value chip (Me / record names) in advanced filters (#23718)

## 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**

<img width="1280" height="760" alt="image"
src="https://github.com/user-attachments/assets/8a867f51-a538-46f2-ba21-a16bb70d85a5"
/>

**Advanced filter**

<img width="1280" height="760" alt="image"
src="https://github.com/user-attachments/assets/b09b9d70-5692-4bac-8cec-3cb006961042"
/>

## 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.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23718?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Charles Bochet
2026-08-04 13:11:53 +02:00
committed by GitHub
parent 453f3479ab
commit 4b3614b413
4 changed files with 146 additions and 84 deletions
@@ -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 (
<SelectControl
selectedOption={{ label: displayValue, value: null }}
textAccent="default"
isDisabled={false}
/>
);
};
@@ -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 (
<AdvancedFilterRelationValueInputClickableSelect
recordFilter={recordFilter}
/>
);
}
return isDateTimeType ? (
<StyledControlContainer>{advancedFilterInputText}</StyledControlContainer>
) : (
@@ -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 };
};
@@ -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,
}),
};
};