Sub-field filtering on ADDRESS type (#11912)
This PR adds what's needed to filter on the ADDRESS sub-fields, notably the country sub-field, that requires a country multi select component, which was created in this PR (ObjectFilterDropdownCountrySelect) This PR refactors the common logic between advanced filter dropdown field selection logic and view bar filter dropdown field selection logic, notably in useFilterDropdownSelectableFieldMetadataItems. There are now new components to identify clearly what's tied to view bar or advanced filter, it could be further simplified or factorized, but as it is right now, it's simple enough to be maintained easily even if a little bit too verbose, which is often the best trade-off we should aim for. Improvements : - Added the CompositeFieldSubFieldName where needed - Fixes bug in advanced filter dropdown input - Fixes dropdown content width bug in advanced filter dropdown input - Fixes a bug when inputing a Currency filter without a sub-field in view bar filter dropdown - Used DropdownMenuSearchInput instead of a custom StyledInput which was doing exactly the same thing - Factorized the state setting logic in useSetAdvancedFilterDropdownStates in an anonymous function setAdvancedFilterDropdownStates - Created useSelectFilterFromViewBarFilterDropdown hook to have a more meaningful and clear logic to abstract what happens when we select a field to filter in the view bard field select dropdown - Fixes a bug with advanced filter operand dropdown select which wasn't modifying the current record filter and creating a stale state. Fixes https://github.com/twentyhq/core-team-issues/issues/612
This commit is contained in:
@ -0,0 +1,81 @@
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
|
||||
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
||||
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
import { AdvancedFilterDropdownFieldSelectMenuItem } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownFieldSelectMenuItem';
|
||||
import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId';
|
||||
import { useFilterDropdownSelectableFieldMetadataItems } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems';
|
||||
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const AdvancedFilterDropdownFieldSelectMenu = () => {
|
||||
const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2(
|
||||
objectFilterDropdownSearchInputComponentState,
|
||||
);
|
||||
|
||||
const objectFilterDropdownSearchInput = useRecoilComponentValueV2(
|
||||
objectFilterDropdownSearchInputComponentState,
|
||||
);
|
||||
|
||||
const {
|
||||
selectableHiddenFieldMetadataItems,
|
||||
selectableVisibleFieldMetadataItems,
|
||||
} = useFilterDropdownSelectableFieldMetadataItems();
|
||||
|
||||
const shouldShowSeparator =
|
||||
selectableVisibleFieldMetadataItems.length > 0 &&
|
||||
selectableHiddenFieldMetadataItems.length > 0;
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const selectableFieldMetadataItemIds = [
|
||||
...selectableVisibleFieldMetadataItems.map(
|
||||
(fieldMetadataItem) => fieldMetadataItem.id,
|
||||
),
|
||||
...selectableHiddenFieldMetadataItems.map(
|
||||
(fieldMetadataItem) => fieldMetadataItem.id,
|
||||
),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSearchInput
|
||||
value={objectFilterDropdownSearchInput}
|
||||
autoFocus
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setObjectFilterDropdownSearchInput(event.target.value)
|
||||
}
|
||||
/>
|
||||
<SelectableList
|
||||
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
|
||||
selectableItemIdArray={selectableFieldMetadataItemIds}
|
||||
selectableListInstanceId={FILTER_FIELD_LIST_ID}
|
||||
>
|
||||
<DropdownMenuItemsContainer>
|
||||
{selectableVisibleFieldMetadataItems.map(
|
||||
(visibleFieldMetadataItem) => (
|
||||
<AdvancedFilterDropdownFieldSelectMenuItem
|
||||
key={visibleFieldMetadataItem.id}
|
||||
fieldMetadataItemToSelect={visibleFieldMetadataItem}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{shouldShowSeparator && <DropdownMenuSeparator />}
|
||||
{selectableHiddenFieldMetadataItems.map((hiddenFieldMetadataItem) => (
|
||||
<AdvancedFilterDropdownFieldSelectMenuItem
|
||||
key={hiddenFieldMetadataItem.id}
|
||||
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -26,13 +26,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
export type ObjectFilterDropdownFilterSelectMenuItemProps = {
|
||||
export type AdvancedFilterDropdownFieldSelectMenuItemProps = {
|
||||
fieldMetadataItemToSelect: FieldMetadataItem;
|
||||
};
|
||||
|
||||
export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
||||
export const AdvancedFilterDropdownFieldSelectMenuItem = ({
|
||||
fieldMetadataItemToSelect,
|
||||
}: ObjectFilterDropdownFilterSelectMenuItemProps) => {
|
||||
}: AdvancedFilterDropdownFieldSelectMenuItemProps) => {
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
@ -5,7 +5,9 @@ import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-d
|
||||
import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
|
||||
import { AdvancedFilterDropdownTextInput } from '@/object-record/advanced-filter/components/AdvancedFilterDropdownTextInput';
|
||||
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
|
||||
import { ObjectFilterDropdownCountrySelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect';
|
||||
import { ObjectFilterDropdownCurrencySelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect';
|
||||
import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput';
|
||||
import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
|
||||
@ -39,6 +41,12 @@ export const AdvancedFilterDropdownFilterInput = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{filterType === 'ADDRESS' &&
|
||||
(subFieldNameUsedInDropdown === 'addressCountry' ? (
|
||||
<ObjectFilterDropdownCountrySelect />
|
||||
) : (
|
||||
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
|
||||
))}
|
||||
{filterType === 'RATING' && <ObjectFilterDropdownRatingInput />}
|
||||
{DATE_FILTER_TYPES.includes(filterType) && (
|
||||
<ObjectFilterDropdownDateInput />
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect';
|
||||
import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId';
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
@ -21,6 +20,7 @@ import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-re
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { isCompositeTypeFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -30,12 +30,13 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { StyledInput } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
export const ObjectFilterDropdownSubFieldSelect = () => {
|
||||
export const AdvancedFilterDropdownSubFieldSelectMenu = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
@ -87,7 +88,7 @@ export const ObjectFilterDropdownSubFieldSelect = () => {
|
||||
|
||||
const handleSelectFilter = (
|
||||
fieldMetadataItem: FieldMetadataItem | null | undefined,
|
||||
subFieldName?: string | null | undefined,
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined,
|
||||
) => {
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return;
|
||||
@ -1,10 +1,7 @@
|
||||
import { useGetFieldMetadataItemById } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||
import { useApplyObjectFilterDropdownOperand } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand';
|
||||
|
||||
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
|
||||
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
@ -41,42 +38,17 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
|
||||
(recordFilter) => recordFilter.id === recordFilterId,
|
||||
);
|
||||
|
||||
const { getFieldMetadataItemById } = useGetFieldMetadataItemById();
|
||||
|
||||
const isDisabled = !filter?.fieldMetadataId;
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
const { applyObjectFilterDropdownOperand } =
|
||||
useApplyObjectFilterDropdownOperand();
|
||||
|
||||
const handleOperandChange = (operand: ViewFilterOperand) => {
|
||||
closeDropdown();
|
||||
|
||||
if (!filter) {
|
||||
throw new Error('Filter is not defined');
|
||||
}
|
||||
|
||||
const fieldMetadataItem = getFieldMetadataItemById(filter.fieldMetadataId);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
throw new Error('Field metadata item is not defined');
|
||||
}
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
const { value, displayValue } = getInitialFilterValue(
|
||||
filterType,
|
||||
operand,
|
||||
filter.value,
|
||||
filter.displayValue,
|
||||
);
|
||||
|
||||
upsertRecordFilter({
|
||||
...filter,
|
||||
operand,
|
||||
value,
|
||||
displayValue,
|
||||
});
|
||||
applyObjectFilterDropdownOperand(operand);
|
||||
};
|
||||
|
||||
const filterType = filter?.type;
|
||||
|
||||
@ -15,6 +15,7 @@ import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/
|
||||
import { areCompositeTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable';
|
||||
import { isCompositeTypeFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeFilterableByAnySubField';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
@ -57,7 +58,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
|
||||
const handleSelectFilter = (
|
||||
selectedFieldMetadataItem: FieldMetadataItem | null | undefined,
|
||||
subFieldName?: string | null | undefined,
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined,
|
||||
) => {
|
||||
if (!isDefined(selectedFieldMetadataItem)) {
|
||||
return;
|
||||
|
||||
@ -7,6 +7,7 @@ import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/consta
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
||||
import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
@ -36,6 +37,10 @@ export const AdvancedFilterValueInput = ({
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const subFieldNameUsedInDropdown = useRecoilComponentValueV2(
|
||||
subFieldNameUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const recordFilter = currentRecordFilters.find(
|
||||
(recordFilter) => recordFilter.id === recordFilterId,
|
||||
);
|
||||
@ -86,7 +91,9 @@ export const AdvancedFilterValueInput = ({
|
||||
FieldMetadataType.CURRENCY,
|
||||
'amountMicros',
|
||||
recordFilter.subFieldName,
|
||||
);
|
||||
) ||
|
||||
(filterType === 'ADDRESS' &&
|
||||
subFieldNameUsedInDropdown !== 'addressCountry');
|
||||
|
||||
return (
|
||||
<StyledValueDropdownContainer>
|
||||
|
||||
@ -11,6 +11,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -20,7 +21,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
type SelectFilterParams = {
|
||||
fieldMetadataItemId: string;
|
||||
recordFilterId: string;
|
||||
subFieldName?: string | null | undefined;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
};
|
||||
|
||||
export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
|
||||
@ -2,8 +2,10 @@ import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/adv
|
||||
import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId';
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
@ -28,26 +30,42 @@ export const useSetAdvancedFilterDropdownStates = () => {
|
||||
recordFilter.recordFilterGroupId === rootLevelRecordFilterGroup?.id,
|
||||
);
|
||||
|
||||
for (const rootLevelRecordFilter of rootLevelRecordFilters) {
|
||||
const setAdvancedFilterStatesForRecordFilter = (
|
||||
recordFilter: RecordFilter,
|
||||
) => {
|
||||
set(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState.atomFamily({
|
||||
instanceId:
|
||||
getAdvancedFilterObjectFilterDropdownComponentInstanceId(
|
||||
rootLevelRecordFilter.id,
|
||||
recordFilter.id,
|
||||
),
|
||||
}),
|
||||
rootLevelRecordFilter,
|
||||
recordFilter,
|
||||
);
|
||||
|
||||
set(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({
|
||||
instanceId:
|
||||
getAdvancedFilterObjectFilterDropdownComponentInstanceId(
|
||||
rootLevelRecordFilter.id,
|
||||
recordFilter.id,
|
||||
),
|
||||
}),
|
||||
rootLevelRecordFilter.fieldMetadataId,
|
||||
recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
set(
|
||||
subFieldNameUsedInDropdownComponentState.atomFamily({
|
||||
instanceId:
|
||||
getAdvancedFilterObjectFilterDropdownComponentInstanceId(
|
||||
recordFilter.id,
|
||||
),
|
||||
}),
|
||||
recordFilter.subFieldName,
|
||||
);
|
||||
};
|
||||
|
||||
for (const rootLevelRecordFilter of rootLevelRecordFilters) {
|
||||
setAdvancedFilterStatesForRecordFilter(rootLevelRecordFilter);
|
||||
}
|
||||
|
||||
const childRecordFilterGroups = currentRecordFilterGroups.filter(
|
||||
@ -63,25 +81,7 @@ export const useSetAdvancedFilterDropdownStates = () => {
|
||||
);
|
||||
|
||||
for (const recordFilterInThisGroup of recordFiltersInThisGroup) {
|
||||
set(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState.atomFamily({
|
||||
instanceId:
|
||||
getAdvancedFilterObjectFilterDropdownComponentInstanceId(
|
||||
recordFilterInThisGroup.id,
|
||||
),
|
||||
}),
|
||||
recordFilterInThisGroup,
|
||||
);
|
||||
|
||||
set(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({
|
||||
instanceId:
|
||||
getAdvancedFilterObjectFilterDropdownComponentInstanceId(
|
||||
recordFilterInThisGroup.id,
|
||||
),
|
||||
}),
|
||||
recordFilterInThisGroup.fieldMetadataId,
|
||||
);
|
||||
setAdvancedFilterStatesForRecordFilter(recordFilterInThisGroup);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||
import { getCountryFlagMenuItemAvatar } from '@/object-record/object-filter-dropdown/utils/getCountryFlagMenuItemAvatar';
|
||||
import { turnCountryIntoSelectableItem } from '@/object-record/object-filter-dropdown/utils/turnCountryIntoSelectableItem';
|
||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { ChangeEvent, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
|
||||
|
||||
export const EMPTY_FILTER_VALUE = '[]';
|
||||
export const MAX_ITEMS_TO_DISPLAY = 5;
|
||||
|
||||
type ObjectFilterDropdownCountrySelectProps = {
|
||||
dropdownWidth?: number;
|
||||
};
|
||||
|
||||
export const ObjectFilterDropdownCountrySelect = ({
|
||||
dropdownWidth,
|
||||
}: ObjectFilterDropdownCountrySelectProps) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValueV2(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
);
|
||||
|
||||
const { applyObjectFilterDropdownFilterValue } =
|
||||
useApplyObjectFilterDropdownFilterValue();
|
||||
|
||||
const fieldMetadataItemUsedInFilterDropdown = useRecoilComponentValueV2(
|
||||
fieldMetadataItemUsedInDropdownComponentSelector,
|
||||
);
|
||||
|
||||
const countries = useCountries();
|
||||
|
||||
const countriesAsSelectableItems = countries.map(
|
||||
turnCountryIntoSelectableItem,
|
||||
);
|
||||
|
||||
const selectedCountryNames = isNonEmptyString(
|
||||
objectFilterDropdownCurrentRecordFilter?.value,
|
||||
)
|
||||
? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse
|
||||
: [];
|
||||
|
||||
const filteredSelectableItems = countriesAsSelectableItems.filter(
|
||||
(selectableItem) =>
|
||||
selectableItem.name.toLowerCase().includes(searchText.toLowerCase()) &&
|
||||
!selectedCountryNames.includes(selectableItem.name),
|
||||
);
|
||||
|
||||
const filteredSelectedItems = countriesAsSelectableItems.filter(
|
||||
(selectableItem) =>
|
||||
selectableItem.name.toLowerCase().includes(searchText.toLowerCase()) &&
|
||||
selectedCountryNames.includes(selectableItem.name),
|
||||
);
|
||||
|
||||
const handleMultipleItemSelectChange = (
|
||||
itemToSelect: SelectableItem,
|
||||
newSelectedValue: boolean,
|
||||
) => {
|
||||
const newSelectedItemNames = newSelectedValue
|
||||
? [...selectedCountryNames, itemToSelect.name]
|
||||
: selectedCountryNames.filter((name) => name !== itemToSelect.name);
|
||||
|
||||
if (!isDefined(fieldMetadataItemUsedInFilterDropdown)) {
|
||||
throw new Error(
|
||||
'Field metadata item used in filter dropdown should be defined',
|
||||
);
|
||||
}
|
||||
|
||||
const selectedItemNames = countriesAsSelectableItems
|
||||
.filter((option) => newSelectedItemNames.includes(option.name))
|
||||
.map((option) => option.name);
|
||||
|
||||
const filterDisplayValue =
|
||||
selectedItemNames.length > MAX_ITEMS_TO_DISPLAY
|
||||
? `${selectedItemNames.length} countries`
|
||||
: selectedItemNames.join(', ');
|
||||
|
||||
const newFilterValue =
|
||||
newSelectedItemNames.length > 0
|
||||
? JSON.stringify(selectedItemNames)
|
||||
: EMPTY_FILTER_VALUE;
|
||||
|
||||
applyObjectFilterDropdownFilterValue(newFilterValue, filterDisplayValue);
|
||||
};
|
||||
|
||||
const showNoResult =
|
||||
filteredSelectableItems.length === 0 &&
|
||||
filteredSelectedItems.length === 0 &&
|
||||
searchText !== '';
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchText}
|
||||
placeholder={t`Search country`}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight width={dropdownWidth ?? 200}>
|
||||
{filteredSelectedItems?.map((item) => {
|
||||
return (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={item.id}
|
||||
selected={true}
|
||||
onSelectChange={(newCheckedValue) => {
|
||||
handleMultipleItemSelectChange(item, newCheckedValue);
|
||||
}}
|
||||
text={item.name}
|
||||
avatar={getCountryFlagMenuItemAvatar(item.name, countries)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredSelectableItems?.map((item) => {
|
||||
return (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={item.id}
|
||||
selected={false}
|
||||
onSelectChange={(newCheckedValue) => {
|
||||
handleMultipleItemSelectChange(item, newCheckedValue);
|
||||
}}
|
||||
text={item.name}
|
||||
avatar={getCountryFlagMenuItemAvatar(item.name, countries)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showNoResult && <MenuItem text={t`No results`} />}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -60,6 +60,8 @@ export const ObjectFilterDropdownCurrencySelect = ({
|
||||
selectedCurrencies.includes(selectableItem.id),
|
||||
);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleMultipleItemSelectChange = (
|
||||
itemToSelect: SelectableItem,
|
||||
newSelectedValue: boolean,
|
||||
@ -78,9 +80,11 @@ export const ObjectFilterDropdownCurrencySelect = ({
|
||||
.filter((option) => newSelectedItemIds.includes(option.id))
|
||||
.map((option) => option.name);
|
||||
|
||||
const currenciesLabel = t`currencies`;
|
||||
|
||||
const filterDisplayValue =
|
||||
selectedItemNames.length > MAX_ITEMS_TO_DISPLAY
|
||||
? `${selectedItemNames.length} currencies`
|
||||
? `${selectedItemNames.length} ${currenciesLabel}`
|
||||
: selectedItemNames.join(', ');
|
||||
|
||||
const newFilterValue =
|
||||
@ -96,8 +100,6 @@ export const ObjectFilterDropdownCurrencySelect = ({
|
||||
filteredSelectedItems.length === 0 &&
|
||||
searchText !== '';
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSearchInput
|
||||
|
||||
@ -75,6 +75,8 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
subFieldNameUsedInDropdown,
|
||||
);
|
||||
|
||||
const isNotASubFieldFilter = !isDefined(subFieldNameUsedInDropdown);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isConfigurable && selectedOperandInDropdown && (
|
||||
@ -99,7 +101,7 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
</>
|
||||
)}
|
||||
{filterType === 'ACTOR' &&
|
||||
(isActorSourceCompositeFilter ? (
|
||||
(isActorSourceCompositeFilter || isNotASubFieldFilter ? (
|
||||
<>
|
||||
<ObjectFilterDropdownSourceSelect />
|
||||
</>
|
||||
@ -108,6 +110,14 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
<ObjectFilterDropdownTextInput />
|
||||
</>
|
||||
))}
|
||||
{filterType === 'ADDRESS' &&
|
||||
(isNotASubFieldFilter ? (
|
||||
<>
|
||||
<ObjectFilterDropdownTextInput />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
))}
|
||||
{filterType === 'CURRENCY' &&
|
||||
(isExpectedSubFieldName(
|
||||
FieldMetadataType.CURRENCY,
|
||||
@ -126,7 +136,7 @@ export const ObjectFilterDropdownFilterInput = ({
|
||||
<ObjectFilterDropdownNumberInput />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
<ObjectFilterDropdownNumberInput />
|
||||
))}
|
||||
{['SELECT', 'MULTI_SELECT'].includes(filterType) && (
|
||||
<>
|
||||
|
||||
@ -5,7 +5,6 @@ export const TEXT_FILTER_TYPES = [
|
||||
'FULL_NAME',
|
||||
'LINK',
|
||||
'LINKS',
|
||||
'ADDRESS',
|
||||
'ARRAY',
|
||||
'RAW_JSON',
|
||||
];
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
||||
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const useFilterDropdownSelectableFieldMetadataItems = () => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const objectFilterDropdownSearchInput = useRecoilComponentValueV2(
|
||||
objectFilterDropdownSearchInputComponentState,
|
||||
);
|
||||
|
||||
const { filterableFieldMetadataItems } =
|
||||
useFilterableFieldMetadataItemsInRecordIndexContext();
|
||||
|
||||
const visibleTableColumns = useRecoilComponentValueV2(
|
||||
visibleTableColumnsComponentSelector,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const visibleColumnsIds = visibleTableColumns.map(
|
||||
(column) => column.fieldMetadataId,
|
||||
);
|
||||
|
||||
const filteredSearchInputFieldMetadataItems =
|
||||
filterableFieldMetadataItems.filter((fieldMetadataItem) =>
|
||||
fieldMetadataItem.label
|
||||
.toLocaleLowerCase()
|
||||
.includes(objectFilterDropdownSearchInput.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
const selectableVisibleFieldMetadataItems =
|
||||
filteredSearchInputFieldMetadataItems
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
visibleColumnsIds.indexOf(a.id) - visibleColumnsIds.indexOf(b.id)
|
||||
);
|
||||
})
|
||||
.filter((fieldMetadataItem) =>
|
||||
visibleColumnsIds.includes(fieldMetadataItem.id),
|
||||
);
|
||||
|
||||
const selectableHiddenFieldMetadataItems =
|
||||
filteredSearchInputFieldMetadataItems
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
!visibleColumnsIds.includes(fieldMetadataItem.id),
|
||||
);
|
||||
|
||||
return {
|
||||
selectableVisibleFieldMetadataItems,
|
||||
selectableHiddenFieldMetadataItems,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,84 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
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 { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSelectFilterFromViewBarFilterDropdown = () => {
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const [, setObjectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2(
|
||||
objectFilterDropdownFilterIsSelectedComponentState,
|
||||
);
|
||||
|
||||
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
|
||||
selectedOperandInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownCurrentRecordFilter =
|
||||
useSetRecoilComponentStateV2(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
);
|
||||
|
||||
const selectFilterFromViewBarFilterDropdown = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id);
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
if (filterType === 'RELATION' || filterType === 'SELECT') {
|
||||
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||
}
|
||||
|
||||
const defaultOperand = getRecordFilterOperands({
|
||||
filterType,
|
||||
})[0];
|
||||
|
||||
setObjectFilterDropdownFilterIsSelected(true);
|
||||
|
||||
const duplicateFilterInCurrentRecordFilters =
|
||||
findDuplicateRecordFilterInNonAdvancedRecordFilters({
|
||||
recordFilters: currentRecordFilters,
|
||||
fieldMetadataItemId: fieldMetadataItem.id,
|
||||
});
|
||||
|
||||
const filterIsAlreadyInCurrentRecordFilters = isDefined(
|
||||
duplicateFilterInCurrentRecordFilters,
|
||||
);
|
||||
|
||||
if (filterIsAlreadyInCurrentRecordFilters) {
|
||||
setObjectFilterDropdownCurrentRecordFilter(
|
||||
duplicateFilterInCurrentRecordFilters,
|
||||
);
|
||||
|
||||
setSelectedOperandInDropdown(
|
||||
duplicateFilterInCurrentRecordFilters.operand,
|
||||
);
|
||||
} else {
|
||||
setSelectedOperandInDropdown(defaultOperand);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
selectFilterFromViewBarFilterDropdown,
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,9 @@
|
||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const subFieldNameUsedInDropdownComponentState = createComponentStateV2<
|
||||
string | null | undefined
|
||||
CompositeFieldSubFieldName | null | undefined
|
||||
>({
|
||||
key: 'subFieldNameUsedInDropdownComponentState',
|
||||
defaultValue: null,
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||
|
||||
export const getCountryFlagMenuItemAvatar = (
|
||||
countryName: string,
|
||||
countries: Country[],
|
||||
): React.ReactNode => {
|
||||
const country = countries.find(
|
||||
(country) => country.countryName === countryName,
|
||||
);
|
||||
|
||||
if (!country) {
|
||||
return <div style={{ width: 20 }} />;
|
||||
}
|
||||
|
||||
return country.Flag({ width: 20 });
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||
|
||||
export const turnCountryIntoSelectableItem = (
|
||||
country: Country,
|
||||
): SelectableItem => ({
|
||||
id: country.countryCode,
|
||||
name: `${country.countryName}`,
|
||||
isSelected: false,
|
||||
});
|
||||
@ -3,7 +3,6 @@ import { useEffect } from 'react';
|
||||
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
@ -13,6 +12,7 @@ import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -118,7 +118,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => {
|
||||
>
|
||||
{t`Group by`}
|
||||
</DropdownMenuHeader>
|
||||
<StyledInput
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={recordGroupFieldSearchInput}
|
||||
placeholder={t`Search fields`}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FilterableFieldType } from '@/object-record/record-filter/types/FilterableFieldType';
|
||||
import { FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
||||
export type RecordFilter = {
|
||||
@ -13,7 +14,7 @@ export type RecordFilter = {
|
||||
operand: ViewFilterOperand;
|
||||
positionInRecordFilterGroup?: number | null;
|
||||
label: string;
|
||||
subFieldName?: string | null | undefined;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
};
|
||||
|
||||
export type RecordFilterToRecordInputOperand<T extends FilterableFieldType> =
|
||||
|
||||
@ -220,31 +220,124 @@ describe('should work as expected for the different field types', () => {
|
||||
{
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressStreet1: {
|
||||
ilike: '%123 Main St%',
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressStreet1: {
|
||||
ilike: '%123 Main St%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
address: {
|
||||
addressStreet1: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressStreet2: {
|
||||
ilike: '%123 Main St%',
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressStreet2: {
|
||||
ilike: '%123 Main St%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
address: {
|
||||
addressStreet2: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressCity: {
|
||||
ilike: '%123 Main St%',
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressCity: {
|
||||
ilike: '%123 Main St%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
address: {
|
||||
addressCity: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressState: {
|
||||
ilike: '%123 Main St%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
address: {
|
||||
addressState: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressPostcode: {
|
||||
ilike: '%123 Main St%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
address: {
|
||||
addressPostcode: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
address: {
|
||||
addressCountry: {
|
||||
ilike: '%123 Main St%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
address: {
|
||||
addressCountry: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ const COMPOSITE_TYPES_FILTERABLE = [
|
||||
'ACTOR',
|
||||
'FULL_NAME',
|
||||
'CURRENCY',
|
||||
'ADDRESS',
|
||||
] satisfies FieldType[];
|
||||
|
||||
type FilterableCompositeFieldType = (typeof COMPOSITE_TYPES_FILTERABLE)[number];
|
||||
|
||||
@ -392,7 +392,8 @@ export const computeFilterRecordGqlOperationFilter = ({
|
||||
FieldMetadataType.CURRENCY,
|
||||
'amountMicros',
|
||||
subFieldName,
|
||||
)
|
||||
) ||
|
||||
!isSubFieldFilter
|
||||
) {
|
||||
switch (filter.operand) {
|
||||
case RecordFilterOperand.GreaterThan:
|
||||
@ -579,6 +580,22 @@ export const computeFilterRecordGqlOperationFilter = ({
|
||||
],
|
||||
};
|
||||
} else {
|
||||
if (subFieldName === 'addressCountry') {
|
||||
const parsedCountryCodes = JSON.parse(filter.value) as string[];
|
||||
|
||||
if (filter.value === '[]' || parsedCountryCodes.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
[subFieldName]: {
|
||||
in: parsedCountryCodes,
|
||||
} as AddressFilter,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
[subFieldName]: {
|
||||
@ -592,43 +609,176 @@ export const computeFilterRecordGqlOperationFilter = ({
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${filter.value}%`,
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressStreet1: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${filter.value}%`,
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressStreet2: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${filter.value}%`,
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressCity: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressState: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressState: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressPostcode: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressPostcode: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressCountry: {
|
||||
ilike: `%${filter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressCountry: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
if (subFieldName === 'addressCountry') {
|
||||
const parsedCountryCodes = JSON.parse(filter.value) as string[];
|
||||
|
||||
if (filter.value === '[]' || parsedCountryCodes.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
addressCountry: {
|
||||
in: JSON.parse(filter.value),
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
addressCountry: {
|
||||
is: 'NULL',
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[subFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
} as AddressFilter,
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[subFieldName]: {
|
||||
ilike: `%${filter.value}%`,
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
[subFieldName]: {
|
||||
is: 'NULL',
|
||||
} as AddressFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
||||
@ -17,17 +17,17 @@ export const getDefaultSubFieldNameForCompositeFilterableFieldType = (
|
||||
case 'CURRENCY':
|
||||
return 'amountMicros';
|
||||
case 'LINKS':
|
||||
return 'primaryLinkUrl';
|
||||
return undefined;
|
||||
case 'PHONES':
|
||||
return 'primaryPhoneNumber';
|
||||
return undefined;
|
||||
case 'EMAILS':
|
||||
return 'primaryEmail';
|
||||
return undefined;
|
||||
case 'ADDRESS':
|
||||
return 'addressCity';
|
||||
return undefined;
|
||||
case 'ACTOR':
|
||||
return 'source';
|
||||
case 'FULL_NAME':
|
||||
return 'firstName';
|
||||
return undefined;
|
||||
default:
|
||||
assertUnreachable(compositeFieldType);
|
||||
}
|
||||
|
||||
@ -164,18 +164,8 @@ export const getRecordFilterOperands = ({
|
||||
)
|
||||
) {
|
||||
return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.currencyCode;
|
||||
} else if (
|
||||
isExpectedSubFieldName(
|
||||
FieldMetadataType.CURRENCY,
|
||||
'amountMicros',
|
||||
subFieldName,
|
||||
)
|
||||
) {
|
||||
return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.amountMicros;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown subfield name ${subFieldName} for ${filterType} filter`,
|
||||
);
|
||||
return COMPOSITE_FIELD_FILTER_OPERANDS_MAP.CURRENCY.amountMicros;
|
||||
}
|
||||
}
|
||||
case 'NUMBER':
|
||||
|
||||
@ -163,7 +163,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowUp, 'k'],
|
||||
[Key.ArrowUp],
|
||||
() => {
|
||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
||||
move('up');
|
||||
@ -173,7 +173,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.ArrowDown, 'j'],
|
||||
[Key.ArrowDown],
|
||||
() => {
|
||||
setHotkeyScopeAndMemorizePreviousScope(TableHotkeyScope.TableFocus);
|
||||
move('down');
|
||||
|
||||
@ -85,7 +85,7 @@ export const MultipleSelectDropdown = ({
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuItemsContainer hasMaxHeight width="auto">
|
||||
{itemsInDropdown?.map((item) => {
|
||||
return (
|
||||
<SelectableListItem
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { AvatarType, IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export type SelectableItem<T = object> = T & {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@ -76,7 +76,7 @@ export const AddressInput = ({
|
||||
const addressStreet2InputRef = useRef<HTMLInputElement>(null);
|
||||
const addressCityInputRef = useRef<HTMLInputElement>(null);
|
||||
const addressStateInputRef = useRef<HTMLInputElement>(null);
|
||||
const addressPostCodeInputRef = useRef<HTMLInputElement>(null);
|
||||
const addressPostcodeInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const inputRefs: {
|
||||
[key in keyof FieldAddressDraftValue]?: RefObject<HTMLInputElement>;
|
||||
@ -85,7 +85,7 @@ export const AddressInput = ({
|
||||
addressStreet2: addressStreet2InputRef,
|
||||
addressCity: addressCityInputRef,
|
||||
addressState: addressStateInputRef,
|
||||
addressPostcode: addressPostCodeInputRef,
|
||||
addressPostcode: addressPostcodeInputRef,
|
||||
};
|
||||
|
||||
const [focusPosition, setFocusPosition] =
|
||||
|
||||
@ -51,6 +51,7 @@ export const ViewBarFilterDropdown = ({
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
dropdownOffset={{ y: 8 }}
|
||||
onClickOutside={handleDropdownClickOutside}
|
||||
dropdownWidth={208}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,27 +1,16 @@
|
||||
import { ObjectFilterDropdownSubFieldSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSubFieldSelect';
|
||||
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton';
|
||||
import { ViewBarFilterDropdownFieldSelectMenu } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu';
|
||||
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
|
||||
import { ObjectFilterDropdownFieldSelect } from '../../object-record/object-filter-dropdown/components/ObjectFilterDropdownFieldSelect';
|
||||
|
||||
export const ViewBarFilterDropdownContent = () => {
|
||||
const [objectFilterDropdownIsSelectingCompositeField] =
|
||||
useRecoilComponentStateV2(
|
||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||
VIEW_BAR_FILTER_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
const [objectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2(
|
||||
objectFilterDropdownFilterIsSelectedComponentState,
|
||||
VIEW_BAR_FILTER_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
const shouldShowCompositeSelectionSubMenu =
|
||||
objectFilterDropdownIsSelectingCompositeField;
|
||||
|
||||
const shouldShowFilterInput = objectFilterDropdownFilterIsSelected;
|
||||
|
||||
return (
|
||||
@ -30,11 +19,9 @@ export const ViewBarFilterDropdownContent = () => {
|
||||
<ObjectFilterOperandSelectAndInput
|
||||
filterDropdownId={VIEW_BAR_FILTER_DROPDOWN_ID}
|
||||
/>
|
||||
) : shouldShowCompositeSelectionSubMenu ? (
|
||||
<ObjectFilterDropdownSubFieldSelect />
|
||||
) : (
|
||||
<>
|
||||
<ObjectFilterDropdownFieldSelect />
|
||||
<ViewBarFilterDropdownFieldSelectMenu />
|
||||
<ViewBarFilterDropdownAdvancedFilterButton />
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -2,20 +2,16 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
|
||||
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
|
||||
|
||||
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
||||
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId';
|
||||
import { useFilterDropdownSelectableFieldMetadataItems } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownSelectableFieldMetadataItems';
|
||||
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
||||
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { ViewBarFilterDropdownFieldSelectMenuItem } from '@/views/components/ViewBarFilterDropdownFieldSelectMenuItem';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const StyledInput = styled.input`
|
||||
@ -44,59 +40,30 @@ export const StyledInput = styled.input`
|
||||
}
|
||||
`;
|
||||
|
||||
export const ObjectFilterDropdownFieldSelect = () => {
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
export const ViewBarFilterDropdownFieldSelectMenu = () => {
|
||||
const [objectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput] =
|
||||
useRecoilComponentStateV2(objectFilterDropdownSearchInputComponentState);
|
||||
|
||||
const { filterableFieldMetadataItems } =
|
||||
useFilterableFieldMetadataItemsInRecordIndexContext();
|
||||
|
||||
const visibleTableColumns = useRecoilComponentValueV2(
|
||||
visibleTableColumnsComponentSelector,
|
||||
recordIndexId,
|
||||
);
|
||||
const visibleColumnsIds = visibleTableColumns.map(
|
||||
(column) => column.fieldMetadataId,
|
||||
);
|
||||
|
||||
const filteredSearchInputFieldMetadataItems =
|
||||
filterableFieldMetadataItems.filter((fieldMetadataItem) =>
|
||||
fieldMetadataItem.label
|
||||
.toLocaleLowerCase()
|
||||
.includes(objectFilterDropdownSearchInput.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
const visibleColumnsFieldMetadataItems = filteredSearchInputFieldMetadataItems
|
||||
.sort((a, b) => {
|
||||
return visibleColumnsIds.indexOf(a.id) - visibleColumnsIds.indexOf(b.id);
|
||||
})
|
||||
.filter((fieldMetadataItem) =>
|
||||
visibleColumnsIds.includes(fieldMetadataItem.id),
|
||||
);
|
||||
|
||||
const hiddenColumnsFieldMetadataItems = filteredSearchInputFieldMetadataItems
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.filter(
|
||||
(fieldMetadataItem) => !visibleColumnsIds.includes(fieldMetadataItem.id),
|
||||
);
|
||||
|
||||
const shouldShowSeparator =
|
||||
visibleColumnsFieldMetadataItems.length > 0 &&
|
||||
hiddenColumnsFieldMetadataItems.length > 0;
|
||||
|
||||
const { t } = useLingui();
|
||||
const {
|
||||
selectableHiddenFieldMetadataItems,
|
||||
selectableVisibleFieldMetadataItems,
|
||||
} = useFilterDropdownSelectableFieldMetadataItems();
|
||||
|
||||
const selectableFieldMetadataItemIds = [
|
||||
...visibleColumnsFieldMetadataItems.map(
|
||||
...selectableVisibleFieldMetadataItems.map(
|
||||
(fieldMetadataItem) => fieldMetadataItem.id,
|
||||
),
|
||||
...hiddenColumnsFieldMetadataItems.map(
|
||||
...selectableHiddenFieldMetadataItems.map(
|
||||
(fieldMetadataItem) => fieldMetadataItem.id,
|
||||
),
|
||||
];
|
||||
|
||||
const shouldShowSeparator =
|
||||
selectableVisibleFieldMetadataItems.length > 0 &&
|
||||
selectableHiddenFieldMetadataItems.length > 0;
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledInput
|
||||
@ -113,15 +80,17 @@ export const ObjectFilterDropdownFieldSelect = () => {
|
||||
selectableListInstanceId={FILTER_FIELD_LIST_ID}
|
||||
>
|
||||
<DropdownMenuItemsContainer>
|
||||
{visibleColumnsFieldMetadataItems.map((visibleFieldMetadataItem) => (
|
||||
<ObjectFilterDropdownFilterSelectMenuItem
|
||||
key={visibleFieldMetadataItem.id}
|
||||
fieldMetadataItemToSelect={visibleFieldMetadataItem}
|
||||
/>
|
||||
))}
|
||||
{selectableVisibleFieldMetadataItems.map(
|
||||
(visibleFieldMetadataItem) => (
|
||||
<ViewBarFilterDropdownFieldSelectMenuItem
|
||||
key={visibleFieldMetadataItem.id}
|
||||
fieldMetadataItemToSelect={visibleFieldMetadataItem}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{shouldShowSeparator && <DropdownMenuSeparator />}
|
||||
{hiddenColumnsFieldMetadataItems.map((hiddenFieldMetadataItem) => (
|
||||
<ObjectFilterDropdownFilterSelectMenuItem
|
||||
{selectableHiddenFieldMetadataItems.map((hiddenFieldMetadataItem) => (
|
||||
<ViewBarFilterDropdownFieldSelectMenuItem
|
||||
key={hiddenFieldMetadataItem.id}
|
||||
fieldMetadataItemToSelect={hiddenFieldMetadataItem}
|
||||
/>
|
||||
@ -0,0 +1,124 @@
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||
import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId';
|
||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||
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 { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
export type ViewBarFilterDropdownFieldSelectMenuItemProps = {
|
||||
fieldMetadataItemToSelect: FieldMetadataItem;
|
||||
};
|
||||
|
||||
export const ViewBarFilterDropdownFieldSelectMenuItem = ({
|
||||
fieldMetadataItemToSelect,
|
||||
}: ViewBarFilterDropdownFieldSelectMenuItemProps) => {
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetRecoilComponentStateV2(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const [, setObjectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2(
|
||||
objectFilterDropdownFilterIsSelectedComponentState,
|
||||
);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(FILTER_FIELD_LIST_ID);
|
||||
|
||||
const isSelectedItem = useRecoilComponentFamilyValueV2(
|
||||
isSelectedItemIdComponentFamilySelector,
|
||||
fieldMetadataItemToSelect.id,
|
||||
);
|
||||
|
||||
const setSelectedOperandInDropdown = useSetRecoilComponentStateV2(
|
||||
selectedOperandInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownCurrentRecordFilter =
|
||||
useSetRecoilComponentStateV2(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
);
|
||||
|
||||
const handleSelectFilter = (fieldMetadataItem: FieldMetadataItem) => {
|
||||
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItem.id);
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
if (filterType === 'RELATION' || filterType === 'SELECT') {
|
||||
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||
}
|
||||
|
||||
const defaultOperand = getRecordFilterOperands({
|
||||
filterType,
|
||||
})[0];
|
||||
|
||||
setObjectFilterDropdownFilterIsSelected(true);
|
||||
|
||||
const duplicateFilterInCurrentRecordFilters =
|
||||
findDuplicateRecordFilterInNonAdvancedRecordFilters({
|
||||
recordFilters: currentRecordFilters,
|
||||
fieldMetadataItemId: fieldMetadataItem.id,
|
||||
});
|
||||
|
||||
const filterIsAlreadyInCurrentRecordFilters = isDefined(
|
||||
duplicateFilterInCurrentRecordFilters,
|
||||
);
|
||||
|
||||
if (filterIsAlreadyInCurrentRecordFilters) {
|
||||
setObjectFilterDropdownCurrentRecordFilter(
|
||||
duplicateFilterInCurrentRecordFilters,
|
||||
);
|
||||
|
||||
setSelectedOperandInDropdown(
|
||||
duplicateFilterInCurrentRecordFilters.operand,
|
||||
);
|
||||
} else {
|
||||
setSelectedOperandInDropdown(defaultOperand);
|
||||
}
|
||||
};
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const Icon = getIcon(fieldMetadataItemToSelect.icon);
|
||||
|
||||
const handleClick = () => {
|
||||
resetSelectedItem();
|
||||
|
||||
handleSelectFilter(fieldMetadataItemToSelect);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectableListItem
|
||||
itemId={fieldMetadataItemToSelect.id}
|
||||
onEnter={handleClick}
|
||||
>
|
||||
<MenuItem
|
||||
focused={isSelectedItem}
|
||||
onClick={handleClick}
|
||||
LeftIcon={Icon}
|
||||
text={fieldMetadataItemToSelect.label}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import { CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { ViewFilterOperand } from './ViewFilterOperand';
|
||||
|
||||
export type ViewFilter = {
|
||||
@ -13,5 +14,5 @@ export type ViewFilter = {
|
||||
viewId?: string;
|
||||
viewFilterGroupId?: string;
|
||||
positionInViewFilterGroup?: number | null;
|
||||
subFieldName?: string | null;
|
||||
subFieldName?: CompositeFieldSubFieldName | null;
|
||||
};
|
||||
|
||||
@ -115,6 +115,16 @@ export class GraphqlQueryFilterFieldParser {
|
||||
subFieldFilter as Record<string, any>,
|
||||
);
|
||||
|
||||
if (
|
||||
ARRAY_OPERATORS.includes(operator) &&
|
||||
(!Array.isArray(value) || value.length === 0)
|
||||
) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Invalid filter value for field ${subFieldKey}. Expected non-empty array`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const { sql, params } = computeWhereConditionParts(
|
||||
operator,
|
||||
objectNameSingular,
|
||||
|
||||
@ -11,7 +11,7 @@ export const PETS_DATA_SEEDS = [
|
||||
addressStreet2: '7344 Haley Loop',
|
||||
addressCity: 'Jacksonstad',
|
||||
addressCountry: 'United States',
|
||||
addressPostCode: '32048-5208',
|
||||
addressPostcode: '32048-5208',
|
||||
addressState: 'North Dakota',
|
||||
},
|
||||
vetPhone: {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
export type IconComponentProps = {
|
||||
|
||||
Reference in New Issue
Block a user