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:
+23
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+13
@@ -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>
|
||||
) : (
|
||||
|
||||
+96
@@ -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 };
|
||||
};
|
||||
+14
-84
@@ -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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user