Compute Filter DisplayValue at runtime on FE (#14148)

<img width="1913" height="979" alt="image"
src="https://github.com/user-attachments/assets/33936329-47b1-487f-94fa-54c45e7843fc"
/>

<img width="1913" height="979" alt="image"
src="https://github.com/user-attachments/assets/6ea992a5-1ac6-49f6-8da1-661ca35144aa"
/>
This commit is contained in:
Charles Bochet
2025-08-29 13:54:53 +02:00
committed by GitHub
parent b5114a5fa3
commit e6c87a360d
7 changed files with 161 additions and 14 deletions
@@ -8,6 +8,7 @@ import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRe
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { EditableFilterChipDropdownContent } from '@/views/components/EditableFilterChipDropdownContent';
import { EditableRelationFilterChip } from '@/views/components/EditableRelationFilterChip';
import { useClearVectorSearchInput } from '@/views/hooks/useClearVectorSearchInput';
import { useSetEditableFilterChipDropdownStates } from '@/views/hooks/useSetEditableFilterChipDropdownStates';
@@ -54,11 +55,19 @@ export const EditableFilterDropdownButton = ({
<Dropdown
dropdownId={recordFilter.id}
clickableComponent={
<EditableFilterChip
recordFilter={recordFilter}
onRemove={handleRemove}
onClick={handleFilterChipClick}
/>
recordFilter.type === 'RELATION' ? (
<EditableRelationFilterChip
recordFilter={recordFilter}
onRemove={handleRemove}
onClick={handleFilterChipClick}
/>
) : (
<EditableFilterChip
recordFilter={recordFilter}
onRemove={handleRemove}
onClick={handleFilterChipClick}
/>
)
}
dropdownComponents={
<EditableFilterChipDropdownContent recordFilterId={recordFilter.id} />
@@ -0,0 +1,42 @@
import { useFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useFieldMetadataItemByIdOrThrow';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { useComputeRecordRelationFilterLabelValue } from '@/views/hooks/useComputeRecordRelationFilterLabelValue';
import { useIcons } from 'twenty-ui/display';
type EditableRelationFilterChipProps = {
recordFilter: RecordFilter;
onRemove: () => void;
onClick?: () => void;
};
export const EditableRelationFilterChip = ({
recordFilter,
onRemove,
onClick,
}: EditableRelationFilterChipProps) => {
const { getIcon } = useIcons();
const { fieldMetadataItem } = useFieldMetadataItemByIdOrThrow(
recordFilter.fieldMetadataId,
);
const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon);
const { labelValue } = useComputeRecordRelationFilterLabelValue({
recordFilter,
});
return (
<SortOrFilterChip
key={recordFilter.id}
testId={recordFilter.id}
labelKey={fieldMetadataItem.label}
labelValue={labelValue}
Icon={FieldMetadataItemIcon}
onRemove={onRemove}
onClick={onClick}
type="filter"
/>
);
};
@@ -95,6 +95,10 @@ const StyledSortValue = styled.span`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledKeyLabelContainer = styled.div`
display: flex;
`;
export type SortOrFilterChipVariant = 'default' | 'danger';
export type SortOrFilterChipType = 'sort' | 'filter';
@@ -134,12 +138,14 @@ export const SortOrFilterChip = ({
<Icon size={theme.icon.size.sm} />
</StyledIcon>
)}
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
{type === 'sort' ? (
<StyledSortValue>{labelValue}</StyledSortValue>
) : (
<StyledFilterValue>{labelValue}</StyledFilterValue>
)}
<StyledKeyLabelContainer>
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
{type === 'sort' ? (
<StyledSortValue>{labelValue}</StyledSortValue>
) : (
<StyledFilterValue>{labelValue}</StyledFilterValue>
)}
</StyledKeyLabelContainer>
<StyledDelete
variant={variant}
onClick={handleDeleteClick}
@@ -151,7 +151,7 @@ export const usePersistViewFieldRecords = () => {
mutation: CREATE_CORE_VIEW_FIELD,
variables: {
input: {
id: viewField.id,
id: v4(),
fieldMetadataId: viewField.fieldMetadataId,
viewId: view.id,
isVisible: viewField.isVisible,
@@ -0,0 +1,91 @@
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 { getRecordFilterLabelValue } from '@/views/utils/getRecordFilterLabelValue';
import { arrayOfUuidOrVariableSchema } from '@/views/view-filter-value/validation-schemas/arrayOfUuidsOrVariablesSchema';
import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
type ObjectFilterDropdownRecordSelectProps = {
recordFilter: RecordFilter;
};
export const useComputeRecordRelationFilterLabelValue = ({
recordFilter,
}: ObjectFilterDropdownRecordSelectProps) => {
const { objectMetadataItems } = useObjectMetadataItems();
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,
});
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
? getRecordFilterLabelValue({
...recordFilter,
displayValue: filterDisplayValue,
})
: getRecordFilterLabelValue(recordFilter),
};
};
-1
View File
@@ -46,7 +46,6 @@
},
"files": [
"dist",
"assets",
"accessibility",
"assets",
"components",
@@ -243,7 +243,7 @@ const computePackageJsonFilesAndExportsConfig = (
return {
exports,
typesVersions: { '*': typesVersionsEntries },
files: ['dist', 'assets', ...entrypoints],
files: ['dist', ...entrypoints],
};
};