Implemented filter on FULL_NAME sub-fields (#11628)
This PR implements what's missing to have sub-field filtering. There is a backend modification to save subFieldName, we just add this field on view filter workspace entity. This PR adds subFieldName where missing in frontend, notably in applyFilter calls, that will be refactored soon. Also fixes a bug in ViewBar where Add Filter button was at the right side of the ViewBar, while it should be right after the chips section. Another bug fixed where we wouldn't delete an empty record filter on dropdown click outside from the view bar, which was already the case where using the filter chip dropdown. <img width="512" alt="image" src="https://github.com/user-attachments/assets/e9a2f8d2-a66f-4800-853a-4df5c6b627a9" /> <img width="495" alt="image" src="https://github.com/user-attachments/assets/7542697b-0689-4095-9c3c-b5e47875355f" /> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,5 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
@ -13,27 +11,24 @@ export const AdvancedFilterDropdownTextInput = ({
|
||||
}: AdvancedFilterDropdownTextInputProps) => {
|
||||
const { applyRecordFilter } = useApplyRecordFilter();
|
||||
|
||||
const [inputValue, setInputValue] = useState(() => recordFilter?.value || '');
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
setInputValue(newValue);
|
||||
|
||||
applyRecordFilter({
|
||||
id: recordFilter.id,
|
||||
fieldMetadataId: recordFilter?.fieldMetadataId ?? '',
|
||||
fieldMetadataId: recordFilter.fieldMetadataId,
|
||||
value: newValue,
|
||||
operand: recordFilter.operand,
|
||||
displayValue: newValue,
|
||||
type: recordFilter.type,
|
||||
label: recordFilter.label,
|
||||
recordFilterGroupId: recordFilter?.recordFilterGroupId,
|
||||
positionInRecordFilterGroup: recordFilter?.positionInRecordFilterGroup,
|
||||
recordFilterGroupId: recordFilter.recordFilterGroupId,
|
||||
positionInRecordFilterGroup: recordFilter.positionInRecordFilterGroup,
|
||||
subFieldName: recordFilter.subFieldName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInputV2
|
||||
value={inputValue}
|
||||
value={recordFilter.value}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter value"
|
||||
fullWidth
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
@ -33,12 +36,25 @@ export const AdvancedFilterFieldSelectDropdownButtonClickableSelect = ({
|
||||
? getIcon(fieldMetadataItem?.icon)
|
||||
: undefined;
|
||||
|
||||
const selectedFieldLabel = recordFilter?.label ?? '';
|
||||
const subFieldLabel =
|
||||
isDefined(fieldMetadataItem) &&
|
||||
isCompositeField(fieldMetadataItem.type) &&
|
||||
isNonEmptyString(recordFilter?.subFieldName) &&
|
||||
isValidSubFieldName(recordFilter.subFieldName)
|
||||
? getCompositeSubFieldLabel(
|
||||
fieldMetadataItem.type,
|
||||
recordFilter.subFieldName,
|
||||
)
|
||||
: '';
|
||||
|
||||
const fieldNameLabel = isNonEmptyString(subFieldLabel)
|
||||
? `${recordFilter?.label} / ${subFieldLabel}`
|
||||
: (recordFilter?.label ?? '');
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
selectedOption={{
|
||||
label: selectedFieldLabel,
|
||||
label: fieldNameLabel,
|
||||
value: null,
|
||||
Icon: fieldIcon,
|
||||
}}
|
||||
|
||||
@ -11,6 +11,7 @@ import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/o
|
||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
|
||||
import { isCompositeFieldTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/isCompositeFieldTypeFilterable';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
@ -79,6 +80,12 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
objectFilterDropdownSubMenuFieldType
|
||||
].filterableSubFields.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const subFieldsAreFilterable =
|
||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||
isCompositeFieldTypeSubFieldsFilterable(
|
||||
fieldMetadataItemUsedInDropdown.type,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
@ -101,8 +108,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
LeftIcon={IconApps}
|
||||
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
|
||||
/>
|
||||
{/* TODO: fix this with a backend field on ViewFilter for composite field filter */}
|
||||
{fieldMetadataItemUsedInDropdown?.type === 'ACTOR' &&
|
||||
{subFieldsAreFilterable &&
|
||||
options.map((subFieldName, index) => (
|
||||
<MenuItem
|
||||
key={`select-filter-${index}`}
|
||||
|
||||
@ -97,7 +97,7 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
existingRecordFilter?.positionInRecordFilterGroup,
|
||||
type: filterType,
|
||||
label: fieldMetadataItem.label,
|
||||
subFieldName,
|
||||
subFieldName: subFieldName ?? null,
|
||||
});
|
||||
|
||||
setSubFieldNameUsedInDropdown(subFieldName);
|
||||
|
||||
@ -2,8 +2,12 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdow
|
||||
import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MultipleFiltersButton } from './MultipleFiltersButton';
|
||||
import { MultipleFiltersDropdownContent } from './MultipleFiltersDropdownContent';
|
||||
|
||||
@ -16,9 +20,25 @@ export const MultipleFiltersDropdownButton = ({
|
||||
}: MultipleFiltersDropdownButtonProps) => {
|
||||
const { resetFilterDropdown } = useResetFilterDropdown();
|
||||
|
||||
const handleDropdownClose = useCallback(() => {
|
||||
const { removeRecordFilter } = useRemoveRecordFilter();
|
||||
|
||||
const selectedFilter = useRecoilComponentValueV2(
|
||||
selectedFilterComponentState,
|
||||
);
|
||||
|
||||
const handleDropdownClickOutside = () => {
|
||||
const recordFilterIsEmpty =
|
||||
isDefined(selectedFilter) &&
|
||||
isRecordFilterConsideredEmpty(selectedFilter);
|
||||
|
||||
if (recordFilterIsEmpty) {
|
||||
removeRecordFilter({ recordFilterId: selectedFilter.id });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropdownClose = () => {
|
||||
resetFilterDropdown();
|
||||
}, [resetFilterDropdown]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@ -28,6 +48,7 @@ export const MultipleFiltersDropdownButton = ({
|
||||
dropdownComponents={<MultipleFiltersDropdownContent />}
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
dropdownOffset={{ y: 8 }}
|
||||
onClickOutside={handleDropdownClickOutside}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -82,6 +82,7 @@ export const ObjectFilterDropdownBooleanSelect = () => {
|
||||
positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup,
|
||||
type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type),
|
||||
label: fieldMetadataItemUsedInDropdown.label,
|
||||
subFieldName: selectedFilter?.subFieldName,
|
||||
});
|
||||
|
||||
setSelectedValue(value);
|
||||
|
||||
@ -63,6 +63,7 @@ export const ObjectFilterDropdownDateInput = () => {
|
||||
positionInRecordFilterGroup: selectedFilter?.positionInRecordFilterGroup,
|
||||
type: getFilterTypeFromFieldType(fieldMetadataItemUsedInDropdown.type),
|
||||
label: fieldMetadataItemUsedInDropdown.label,
|
||||
subFieldName: selectedFilter?.subFieldName,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropd
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { isCompositeFieldTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/isCompositeFieldTypeFilterable';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
@ -142,6 +143,12 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
||||
item.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
const subFieldsAreFilterable =
|
||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||
isCompositeFieldTypeSubFieldsFilterable(
|
||||
fieldMetadataItemUsedInDropdown.type,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
@ -172,8 +179,7 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
|
||||
LeftIcon={IconApps}
|
||||
text={`Any ${getFilterableFieldTypeLabel(objectFilterDropdownSubMenuFieldType)} field`}
|
||||
/>
|
||||
{/* TODO: fix this with a backend field on ViewFilter for composite field filter */}
|
||||
{fieldMetadataItemUsedInDropdown?.type === 'ACTOR' &&
|
||||
{subFieldsAreFilterable &&
|
||||
options.map((subFieldName, index) => (
|
||||
<MenuItem
|
||||
key={`select-filter-${index}`}
|
||||
|
||||
@ -70,6 +70,7 @@ export const ObjectFilterDropdownNumberInput = () => {
|
||||
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
|
||||
positionInRecordFilterGroup:
|
||||
selectedFilter?.positionInRecordFilterGroup,
|
||||
subFieldName: selectedFilter?.subFieldName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -147,6 +147,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
||||
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
|
||||
positionInRecordFilterGroup:
|
||||
selectedFilter?.positionInRecordFilterGroup,
|
||||
subFieldName: selectedFilter?.subFieldName,
|
||||
});
|
||||
}
|
||||
resetSelectedItem();
|
||||
|
||||
@ -77,6 +77,7 @@ export const ObjectFilterDropdownRatingInput = () => {
|
||||
fieldMetadataItemUsedInDropdown.type,
|
||||
),
|
||||
label: fieldMetadataItemUsedInDropdown.label,
|
||||
subFieldName: selectedFilter?.subFieldName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -22,8 +22,8 @@ import { RelationFilterValue } from '@/views/view-filter-value/types/RelationFil
|
||||
import { jsonRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/jsonRelationFilterValueSchema';
|
||||
import { simpleRelationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/simpleRelationFilterValueSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { IconUserCircle } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const EMPTY_FILTER_VALUE: string = JSON.stringify({
|
||||
isCurrentWorkspaceMemberSelected: false,
|
||||
@ -235,6 +235,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
duplicateFilterInCurrentRecordFilters.recordFilterGroupId,
|
||||
positionInRecordFilterGroup:
|
||||
duplicateFilterInCurrentRecordFilters.positionInRecordFilterGroup,
|
||||
subFieldName: duplicateFilterInCurrentRecordFilters.subFieldName,
|
||||
});
|
||||
} else {
|
||||
applyRecordFilter({
|
||||
@ -250,6 +251,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
|
||||
positionInRecordFilterGroup:
|
||||
selectedFilter?.positionInRecordFilterGroup,
|
||||
subFieldName: selectedFilter?.subFieldName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldM
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -19,6 +20,10 @@ export const ObjectFilterDropdownTextInput = () => {
|
||||
fieldMetadataItemUsedInDropdownComponentSelector,
|
||||
);
|
||||
|
||||
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
|
||||
subFieldNameUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const selectedFilter = useRecoilComponentValueV2(
|
||||
selectedFilterComponentState,
|
||||
);
|
||||
@ -70,6 +75,7 @@ export const ObjectFilterDropdownTextInput = () => {
|
||||
recordFilterGroupId: selectedFilter?.recordFilterGroupId,
|
||||
positionInRecordFilterGroup:
|
||||
selectedFilter?.positionInRecordFilterGroup,
|
||||
subFieldName: subFieldNameUsedInDropdown,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -71,6 +71,7 @@ export const ObjectFilterDropdownTextSearchInput = () => {
|
||||
label: fieldMetadataItemUsedInDropdown.label,
|
||||
positionInRecordFilterGroup:
|
||||
selectedFilter?.positionInRecordFilterGroup,
|
||||
subFieldName: selectedFilter?.subFieldName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { FieldType } from '@/settings/data-model/types/FieldType';
|
||||
|
||||
type CompositeFilterableFieldType = Extract<FieldType, 'ACTOR' | 'FULL_NAME'>;
|
||||
|
||||
export const isCompositeFieldTypeSubFieldsFilterable = (
|
||||
fieldType: FieldType,
|
||||
): fieldType is CompositeFilterableFieldType => {
|
||||
return (
|
||||
['ACTOR', 'FULL_NAME'] satisfies CompositeFilterableFieldType[]
|
||||
).includes(fieldType as any);
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const isRecordFilterConsideredEmpty = (
|
||||
recordFilter: RecordFilter,
|
||||
): boolean => {
|
||||
const { value, operand } = recordFilter;
|
||||
|
||||
if (
|
||||
(!isDefined(value) || value === '') &&
|
||||
![
|
||||
RecordFilterOperand.IsEmpty,
|
||||
RecordFilterOperand.IsNotEmpty,
|
||||
RecordFilterOperand.IsInPast,
|
||||
RecordFilterOperand.IsInFuture,
|
||||
RecordFilterOperand.IsToday,
|
||||
].includes(operand)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFieldType';
|
||||
|
||||
export const ALL_SUB_FIELDS = COMPOSITE_FIELD_TYPES.flatMap(
|
||||
(compositeFieldType) =>
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields,
|
||||
);
|
||||
@ -11,7 +11,6 @@ import {
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
|
||||
import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import {
|
||||
IllustrationIconCurrency,
|
||||
@ -23,6 +22,7 @@ import {
|
||||
IllustrationIconText,
|
||||
IllustrationIconUser,
|
||||
} from 'twenty-ui/display';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type SettingsCompositeFieldTypeConfig<T> = SettingsFieldTypeConfig<T> & {
|
||||
subFields: (keyof T)[];
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { ALL_SUB_FIELDS } from '@/settings/data-model/constants/AllSubFields';
|
||||
|
||||
export type CompositeFieldSubFieldName = (typeof ALL_SUB_FIELDS)[number];
|
||||
@ -0,0 +1,14 @@
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { COMPOSITE_FIELD_TYPES } from '@/settings/data-model/types/CompositeFieldType';
|
||||
|
||||
export const isValidSubFieldName = (
|
||||
subFieldName: string,
|
||||
): subFieldName is CompositeFieldSubFieldName => {
|
||||
const allSubFields = COMPOSITE_FIELD_TYPES.flatMap(
|
||||
(compositeFieldType) =>
|
||||
SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType].subFields,
|
||||
);
|
||||
|
||||
return allSubFields.includes(subFieldName as any);
|
||||
};
|
||||
@ -1,37 +1,56 @@
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
|
||||
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type EditableFilterChipProps = {
|
||||
viewFilter: RecordFilter;
|
||||
recordFilter: RecordFilter;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
export const EditableFilterChip = ({
|
||||
viewFilter,
|
||||
recordFilter,
|
||||
onRemove,
|
||||
}: EditableFilterChipProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
viewFilter.fieldMetadataId,
|
||||
recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
const FieldMetadataItemIcon = getIcon(fieldMetadataItem.icon);
|
||||
|
||||
const operandLabelShort = getOperandLabelShort(viewFilter.operand);
|
||||
const operandLabelShort = getOperandLabelShort(recordFilter.operand);
|
||||
|
||||
const labelKey = `${viewFilter.label}${isNonEmptyString(viewFilter.value) ? operandLabelShort : ''}`;
|
||||
const recordFilterSubFieldName = recordFilter.subFieldName;
|
||||
|
||||
const subFieldLabel =
|
||||
isCompositeField(fieldMetadataItem.type) &&
|
||||
isNonEmptyString(recordFilterSubFieldName) &&
|
||||
isValidSubFieldName(recordFilterSubFieldName)
|
||||
? getCompositeSubFieldLabel(
|
||||
fieldMetadataItem.type,
|
||||
recordFilterSubFieldName,
|
||||
)
|
||||
: '';
|
||||
|
||||
const fieldNameLabel = isNonEmptyString(subFieldLabel)
|
||||
? `${recordFilter.label} / ${subFieldLabel}`
|
||||
: recordFilter.label;
|
||||
|
||||
const labelKey = `${fieldNameLabel}${isNonEmptyString(recordFilter.value) ? operandLabelShort : ''}`;
|
||||
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
key={viewFilter.id}
|
||||
testId={viewFilter.id}
|
||||
key={recordFilter.id}
|
||||
testId={recordFilter.id}
|
||||
labelKey={labelKey}
|
||||
labelValue={viewFilter.displayValue}
|
||||
labelValue={recordFilter.displayValue}
|
||||
Icon={FieldMetadataItemIcon}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
|
||||
@ -8,7 +8,7 @@ import { EditableFilterChip } from '@/views/components/EditableFilterChip';
|
||||
|
||||
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
|
||||
import { EditableFilterDropdownButtonEffect } from '@/views/components/EditableFilterDropdownButtonEffect';
|
||||
|
||||
type EditableFilterDropdownButtonProps = {
|
||||
@ -31,17 +31,9 @@ export const EditableFilterDropdownButton = ({
|
||||
};
|
||||
|
||||
const handleDropdownClickOutside = useCallback(() => {
|
||||
const { value, operand } = recordFilter;
|
||||
if (
|
||||
!value &&
|
||||
![
|
||||
RecordFilterOperand.IsEmpty,
|
||||
RecordFilterOperand.IsNotEmpty,
|
||||
RecordFilterOperand.IsInPast,
|
||||
RecordFilterOperand.IsInFuture,
|
||||
RecordFilterOperand.IsToday,
|
||||
].includes(operand)
|
||||
) {
|
||||
const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter);
|
||||
|
||||
if (recordFilterIsEmpty) {
|
||||
removeRecordFilter({ recordFilterId: recordFilter.id });
|
||||
}
|
||||
}, [recordFilter, removeRecordFilter]);
|
||||
@ -53,7 +45,7 @@ export const EditableFilterDropdownButton = ({
|
||||
dropdownId={recordFilter.id}
|
||||
clickableComponent={
|
||||
<EditableFilterChip
|
||||
viewFilter={recordFilter}
|
||||
recordFilter={recordFilter}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -70,12 +70,11 @@ const StyledActionButtonContainer = styled.div`
|
||||
`;
|
||||
|
||||
const StyledFilterContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
const StyledSeperatorContainer = styled.div`
|
||||
|
||||
@ -58,6 +58,7 @@ export const usePersistViewFilterRecords = () => {
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
} satisfies Partial<ViewFilter>,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
@ -98,6 +99,7 @@ export const usePersistViewFilterRecords = () => {
|
||||
operand: viewFilter.operand,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
} satisfies Partial<ViewFilter>,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
|
||||
@ -13,4 +13,5 @@ export type ViewFilter = {
|
||||
viewId?: string;
|
||||
viewFilterGroupId?: string;
|
||||
positionInViewFilterGroup?: number | null;
|
||||
subFieldName?: string | null;
|
||||
};
|
||||
|
||||
@ -12,6 +12,7 @@ export const areViewFiltersEqual = (
|
||||
'value',
|
||||
'displayValue',
|
||||
'operand',
|
||||
'subFieldName',
|
||||
];
|
||||
|
||||
return propertiesToCompare.every((property) =>
|
||||
|
||||
@ -13,5 +13,6 @@ export const mapRecordFilterToViewFilter = (
|
||||
value: recordFilter.value,
|
||||
positionInViewFilterGroup: recordFilter.positionInRecordFilterGroup,
|
||||
viewFilterGroupId: recordFilter.recordFilterGroupId,
|
||||
subFieldName: recordFilter.subFieldName,
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,8 +3,8 @@ import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { ViewFilter } from '../types/ViewFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ViewFilter } from '../types/ViewFilter';
|
||||
|
||||
export const mapViewFiltersToFilters = (
|
||||
viewFilters: ViewFilter[],
|
||||
@ -36,6 +36,7 @@ export const mapViewFiltersToFilters = (
|
||||
positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
label: availableFieldMetadataItem.label,
|
||||
type: filterType,
|
||||
subFieldName: viewFilter.subFieldName,
|
||||
} satisfies RecordFilter;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
@ -408,6 +408,7 @@ export const VIEW_FILTER_STANDARD_FIELD_IDS = {
|
||||
view: '20202020-4f5b-487e-829c-3d881c163611',
|
||||
viewFilterGroupId: '20202020-2580-420a-8328-cab1635c0296',
|
||||
positionInViewFilterGroup: '20202020-3bb0-4f66-a537-a46fe0dc468f',
|
||||
subFieldName: '20202020-3bb0-4f66-a537-a46fe0dc469a',
|
||||
};
|
||||
|
||||
export const VIEW_FILTER_GROUP_STANDARD_FIELD_IDS = {
|
||||
|
||||
@ -94,4 +94,15 @@ export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
positionInViewFilterGroup: number | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: VIEW_FILTER_STANDARD_FIELD_IDS.subFieldName,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: msg`Sub field name`,
|
||||
description: msg`Sub field name`,
|
||||
icon: 'IconSubtask',
|
||||
defaultValue: null,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
subFieldName: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user