mirror of
https://github.com/twentyhq/twenty.git
synced 2026-05-24 16:32:28 -04:00
refactor(filters): split advanced-filter sub-menu, derive relation target on demand
- RecordFilter only carries relationTargetFieldMetadataId; derive label/type at render - Split AdvancedFilterSubFieldSelectMenu into Composite + RelationTargetField components - Dedicated objectFilterDropdownIsSelectingRelationTargetField signal (no RELATION sentinel) - mapViewFiltersToFilters takes a single fieldMetadataItems list - Server view-query-params and common-group-by stop embedding resolved relation target - Lift focus-stack push out of useSelectFieldUsedInAdvancedFilterDropdown
This commit is contained in:
@@ -1,16 +1,6 @@
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { AdvancedFilterRelationSubMenu } from '@/object-record/advanced-filter/components/AdvancedFilterRelationSubMenu';
|
||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
||||
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
|
||||
import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType';
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
@@ -23,21 +13,26 @@ import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/Com
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type AdvancedFilterSubFieldSelectMenuProps = {
|
||||
type AdvancedFilterCompositeSubFieldSelectMenuProps = {
|
||||
recordFilterId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
export const AdvancedFilterCompositeSubFieldSelectMenu = ({
|
||||
recordFilterId,
|
||||
}: AdvancedFilterSubFieldSelectMenuProps) => {
|
||||
}: AdvancedFilterCompositeSubFieldSelectMenuProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const fieldMetadataItemUsedInDropdown = useAtomComponentSelectorValue(
|
||||
@@ -63,20 +58,14 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
const handleSelectFilter = ({
|
||||
fieldMetadataItem,
|
||||
subFieldName,
|
||||
relationTargetFieldMetadataItem,
|
||||
}: {
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
subFieldName?: CompositeFieldSubFieldName | null;
|
||||
relationTargetFieldMetadataItem?: FieldMetadataItem | null;
|
||||
}) => {
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: fieldMetadataItem.id,
|
||||
recordFilterId,
|
||||
subFieldName,
|
||||
relationTargetFieldMetadataItem,
|
||||
// The value picker that opens next is for a sub-field or target
|
||||
// field, not the source — its dropdown manages its own focus.
|
||||
skipFocusPush: true,
|
||||
});
|
||||
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
@@ -99,35 +88,8 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE &&
|
||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||
isManyToOneRelationField(fieldMetadataItemUsedInDropdown)
|
||||
) {
|
||||
return (
|
||||
<AdvancedFilterRelationSubMenu
|
||||
recordFilterId={recordFilterId}
|
||||
relationFieldMetadataItem={fieldMetadataItemUsedInDropdown}
|
||||
targetObjectMetadataId={
|
||||
fieldMetadataItemUsedInDropdown.relation.targetObjectMetadata.id
|
||||
}
|
||||
onBack={handleSubMenuBack}
|
||||
onSelectTargetField={handleSelectFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const compositeSubMenuFieldType =
|
||||
objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE
|
||||
? null
|
||||
: objectFilterDropdownSubMenuFieldType;
|
||||
|
||||
if (!isDefined(compositeSubMenuFieldType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subFieldNames = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[
|
||||
compositeSubMenuFieldType
|
||||
objectFilterDropdownSubMenuFieldType
|
||||
].subFields
|
||||
.filter((subField) => subField.isFilterable === true)
|
||||
.map((subField) => subField.subFieldName);
|
||||
@@ -216,7 +178,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
});
|
||||
}}
|
||||
text={getCompositeSubFieldLabel(
|
||||
compositeSubMenuFieldType,
|
||||
objectFilterDropdownSubMenuFieldType,
|
||||
subFieldName,
|
||||
)}
|
||||
LeftIcon={getIcon(
|
||||
@@ -1,7 +1,9 @@
|
||||
import { AdvancedFilterCompositeSubFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterCompositeSubFieldSelectMenu';
|
||||
import { AdvancedFilterFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectMenu';
|
||||
import { AdvancedFilterSubFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterSubFieldSelectMenu';
|
||||
import { AdvancedFilterRelationTargetFieldSelectMenu } from '@/object-record/advanced-filter/components/AdvancedFilterRelationTargetFieldSelectMenu';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
|
||||
type AdvancedFilterFieldSelectDropdownContentProps = {
|
||||
recordFilterId: string;
|
||||
@@ -10,16 +12,29 @@ type AdvancedFilterFieldSelectDropdownContentProps = {
|
||||
export const AdvancedFilterFieldSelectDropdownContent = ({
|
||||
recordFilterId,
|
||||
}: AdvancedFilterFieldSelectDropdownContentProps) => {
|
||||
const [objectFilterDropdownIsSelectingCompositeField] = useAtomComponentState(
|
||||
const isSelectingCompositeField = useAtomComponentStateValue(
|
||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||
);
|
||||
|
||||
const shouldShowCompositeSelectionSubMenu =
|
||||
objectFilterDropdownIsSelectingCompositeField;
|
||||
|
||||
return shouldShowCompositeSelectionSubMenu ? (
|
||||
<AdvancedFilterSubFieldSelectMenu recordFilterId={recordFilterId} />
|
||||
) : (
|
||||
<AdvancedFilterFieldSelectMenu recordFilterId={recordFilterId} />
|
||||
const isSelectingRelationTargetField = useAtomComponentStateValue(
|
||||
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
|
||||
);
|
||||
|
||||
if (isSelectingRelationTargetField) {
|
||||
return (
|
||||
<AdvancedFilterRelationTargetFieldSelectMenu
|
||||
recordFilterId={recordFilterId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSelectingCompositeField) {
|
||||
return (
|
||||
<AdvancedFilterCompositeSubFieldSelectMenu
|
||||
recordFilterId={recordFilterId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AdvancedFilterFieldSelectMenu recordFilterId={recordFilterId} />;
|
||||
};
|
||||
|
||||
@@ -16,9 +16,8 @@ import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/co
|
||||
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType';
|
||||
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
|
||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
import { type ObjectFilterDropdownSubMenuFieldType } from '@/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType';
|
||||
import { isCompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFilterableFieldType';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
@@ -26,6 +25,8 @@ import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/h
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -102,10 +103,17 @@ export const AdvancedFilterFieldSelectMenu = ({
|
||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||
);
|
||||
|
||||
const [, setObjectFilterDropdownIsSelectingRelationTargetField] =
|
||||
useAtomComponentState(
|
||||
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
|
||||
);
|
||||
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const handleFieldMetadataItemSelect = (
|
||||
selectedFieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
@@ -115,30 +123,50 @@ export const AdvancedFilterFieldSelectMenu = ({
|
||||
selectedFieldMetadataItem.type,
|
||||
);
|
||||
|
||||
const subMenuType: ObjectFilterDropdownSubMenuFieldType | null =
|
||||
isManyToOneRelationField(selectedFieldMetadataItem)
|
||||
? RELATION_SUB_MENU_FIELD_TYPE
|
||||
: isCompositeFilterableFieldType(filterType)
|
||||
? filterType
|
||||
: null;
|
||||
const isRelationTraversalField = isManyToOneRelationField(
|
||||
selectedFieldMetadataItem,
|
||||
);
|
||||
|
||||
const compositeSubMenuFieldType =
|
||||
!isRelationTraversalField && isCompositeFilterableFieldType(filterType)
|
||||
? filterType
|
||||
: null;
|
||||
|
||||
// Sub-menus (composite or relation traversal) drive their own focus
|
||||
// scope; pushing the source field id on the focus stack would shadow
|
||||
// their selectable list hotkeys.
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: selectedFieldMetadataItem.id,
|
||||
recordFilterId,
|
||||
skipFocusPush: subMenuType !== null,
|
||||
});
|
||||
|
||||
if (subMenuType === null) {
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
if (isRelationTraversalField) {
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownIsSelectingRelationTargetField(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setObjectFilterDropdownSubMenuFieldType(subMenuType);
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownIsSelectingCompositeField(true);
|
||||
if (compositeSubMenuFieldType !== null) {
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownSubMenuFieldType(compositeSubMenuFieldType);
|
||||
setObjectFilterDropdownIsSelectingCompositeField(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Leaf RELATION/SELECT fields open a value picker keyed by the source
|
||||
// field id — push it on the focus stack so the picker's keyboard
|
||||
// hotkeys are active when it opens.
|
||||
if (
|
||||
selectedFieldMetadataItem.type === 'RELATION' ||
|
||||
selectedFieldMetadataItem.type === 'SELECT'
|
||||
) {
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: selectedFieldMetadataItem.id,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: selectedFieldMetadataItem.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
};
|
||||
|
||||
const shouldShowVisibleFields = visibleFieldMetadataItems.length > 0;
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
||||
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type AdvancedFilterRelationSubMenuProps = {
|
||||
recordFilterId: string;
|
||||
relationFieldMetadataItem: FieldMetadataItem;
|
||||
targetObjectMetadataId: string;
|
||||
onBack: () => void;
|
||||
onSelectTargetField: (params: {
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
relationTargetFieldMetadataItem: FieldMetadataItem;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export const AdvancedFilterRelationSubMenu = ({
|
||||
recordFilterId,
|
||||
relationFieldMetadataItem,
|
||||
targetObjectMetadataId,
|
||||
onBack,
|
||||
onSelectTargetField,
|
||||
}: AdvancedFilterRelationSubMenuProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { advancedFilterFieldSelectDropdownId } =
|
||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
advancedFilterFieldSelectDropdownId,
|
||||
);
|
||||
|
||||
const { filterableFieldMetadataItems: relationTargetFields } =
|
||||
useFilterableFieldMetadataItems(targetObjectMetadataId);
|
||||
|
||||
const selectableItemIdArray = relationTargetFields.map((field) => field.id);
|
||||
|
||||
return (
|
||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={onBack}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{relationFieldMetadataItem.label}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
focusId={advancedFilterFieldSelectDropdownId}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
|
||||
>
|
||||
{relationTargetFields.map((targetField, index) => (
|
||||
<SelectableListItem
|
||||
itemId={targetField.id}
|
||||
key={`select-filter-relation-${index}`}
|
||||
onEnter={() => {
|
||||
onSelectTargetField({
|
||||
fieldMetadataItem: relationFieldMetadataItem,
|
||||
relationTargetFieldMetadataItem: targetField,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === targetField.id}
|
||||
key={`select-filter-relation-${index}`}
|
||||
testId={`select-filter-relation-${index}`}
|
||||
onClick={() => {
|
||||
onSelectTargetField({
|
||||
fieldMetadataItem: relationFieldMetadataItem,
|
||||
relationTargetFieldMetadataItem: targetField,
|
||||
});
|
||||
}}
|
||||
text={targetField.label}
|
||||
LeftIcon={getIcon(targetField.icon)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
||||
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
|
||||
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type AdvancedFilterRelationTargetFieldSelectMenuProps = {
|
||||
recordFilterId: string;
|
||||
};
|
||||
|
||||
export const AdvancedFilterRelationTargetFieldSelectMenu = ({
|
||||
recordFilterId,
|
||||
}: AdvancedFilterRelationTargetFieldSelectMenuProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const sourceFieldMetadataItem = useAtomComponentSelectorValue(
|
||||
fieldMetadataItemUsedInDropdownComponentSelector,
|
||||
);
|
||||
|
||||
const setIsSelectingRelationTargetField = useSetAtomComponentState(
|
||||
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
|
||||
);
|
||||
|
||||
const { closeAdvancedFilterFieldSelectDropdown } =
|
||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||
|
||||
const { selectFieldUsedInAdvancedFilterDropdown } =
|
||||
useSelectFieldUsedInAdvancedFilterDropdown();
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const { advancedFilterFieldSelectDropdownId } =
|
||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
advancedFilterFieldSelectDropdownId,
|
||||
);
|
||||
|
||||
const targetObjectMetadataId =
|
||||
isDefined(sourceFieldMetadataItem) &&
|
||||
isManyToOneRelationField(sourceFieldMetadataItem)
|
||||
? sourceFieldMetadataItem.relation.targetObjectMetadata.id
|
||||
: null;
|
||||
|
||||
const { filterableFieldMetadataItems: relationTargetFields } =
|
||||
useFilterableFieldMetadataItems(targetObjectMetadataId ?? '');
|
||||
|
||||
if (
|
||||
!isDefined(sourceFieldMetadataItem) ||
|
||||
!isManyToOneRelationField(sourceFieldMetadataItem)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSubMenuBack = () => {
|
||||
setIsSelectingRelationTargetField(false);
|
||||
};
|
||||
|
||||
const handleSelectTargetField = (
|
||||
relationTargetFieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: sourceFieldMetadataItem.id,
|
||||
recordFilterId,
|
||||
relationTargetFieldMetadataItem,
|
||||
});
|
||||
|
||||
// Leaf RELATION/SELECT target fields open a value picker keyed by the
|
||||
// target field id — push it on the focus stack so the picker's
|
||||
// keyboard hotkeys are active when it opens.
|
||||
if (
|
||||
relationTargetFieldMetadataItem.type === 'RELATION' ||
|
||||
relationTargetFieldMetadataItem.type === 'SELECT'
|
||||
) {
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: relationTargetFieldMetadataItem.id,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: relationTargetFieldMetadataItem.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setIsSelectingRelationTargetField(false);
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
};
|
||||
|
||||
const selectableItemIdArray = relationTargetFields.map((field) => field.id);
|
||||
|
||||
return (
|
||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={handleSubMenuBack}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{sourceFieldMetadataItem.label}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
focusId={advancedFilterFieldSelectDropdownId}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
|
||||
>
|
||||
{relationTargetFields.map((targetField, index) => (
|
||||
<SelectableListItem
|
||||
itemId={targetField.id}
|
||||
key={`select-filter-relation-${index}`}
|
||||
onEnter={() => {
|
||||
handleSelectTargetField(targetField);
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === targetField.id}
|
||||
key={`select-filter-relation-${index}`}
|
||||
testId={`select-filter-relation-${index}`}
|
||||
onClick={() => {
|
||||
handleSelectTargetField(targetField);
|
||||
}}
|
||||
text={targetField.label}
|
||||
LeftIcon={getIcon(targetField.icon)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
@@ -82,7 +82,7 @@ export const AdvancedFilterValueInput = ({
|
||||
setObjectFilterDropdownCurrentRecordFilter(recordFilter);
|
||||
setFieldMetadataItemIdUsedInDropdown(recordFilter.fieldMetadataId);
|
||||
setRelationTargetFieldMetadataIdUsedInDropdown(
|
||||
recordFilter.relationTargetField?.id ?? null,
|
||||
recordFilter.relationTargetFieldMetadataId ?? null,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { isCompositeTypeNonFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField';
|
||||
import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
|
||||
@@ -26,10 +24,6 @@ type SelectFilterParams = {
|
||||
recordFilterId: string;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
relationTargetFieldMetadataItem?: FieldMetadataItem | null | undefined;
|
||||
// Set when the next step is another dropdown that manages its own focus
|
||||
// (e.g. composite or relation-traversal sub-menus). The default push of
|
||||
// the source field id on the focus stack would shadow it.
|
||||
skipFocusPush?: boolean;
|
||||
};
|
||||
|
||||
export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
@@ -49,8 +43,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const { getFieldMetadataItemByIdOrThrow } =
|
||||
useGetFieldMetadataItemByIdOrThrow();
|
||||
|
||||
@@ -75,7 +67,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
recordFilterId,
|
||||
subFieldName,
|
||||
relationTargetFieldMetadataItem,
|
||||
skipFocusPush,
|
||||
}: SelectFilterParams) => {
|
||||
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId);
|
||||
|
||||
@@ -86,20 +77,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
skipFocusPush !== true &&
|
||||
(fieldMetadataItem.type === 'RELATION' ||
|
||||
fieldMetadataItem.type === 'SELECT')
|
||||
) {
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: fieldMetadataItem.id,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: fieldMetadataItem.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isRelationTraversal = isDefined(relationTargetFieldMetadataItem);
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(
|
||||
@@ -157,13 +134,8 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
? `${fieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}`
|
||||
: fieldMetadataItem.label;
|
||||
|
||||
const relationTargetField = isRelationTraversal
|
||||
? {
|
||||
id: relationTargetFieldMetadataItem.id,
|
||||
name: relationTargetFieldMetadataItem.name,
|
||||
type: relationTargetFieldMetadataItem.type,
|
||||
label: relationTargetFieldMetadataItem.label,
|
||||
}
|
||||
const relationTargetFieldMetadataId = isRelationTraversal
|
||||
? relationTargetFieldMetadataItem.id
|
||||
: null;
|
||||
|
||||
const newAdvancedFilter = {
|
||||
@@ -178,12 +150,12 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
type: filterType,
|
||||
label,
|
||||
subFieldName: subFieldNameToUse,
|
||||
relationTargetField,
|
||||
relationTargetFieldMetadataId,
|
||||
} satisfies RecordFilter;
|
||||
|
||||
setSubFieldNameUsedInDropdown(subFieldNameToUse);
|
||||
setRelationTargetFieldMetadataIdUsedInDropdown(
|
||||
relationTargetField?.id ?? null,
|
||||
relationTargetFieldMetadataId,
|
||||
);
|
||||
|
||||
setObjectFilterDropdownSearchInput('');
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const RELATION_SUB_MENU_FIELD_TYPE = 'RELATION' as const;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
export const objectFilterDropdownIsSelectingRelationTargetFieldComponentState =
|
||||
createAtomComponentState<boolean>({
|
||||
key: 'objectFilterDropdownIsSelectingRelationTargetFieldComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
|
||||
});
|
||||
@@ -156,7 +156,8 @@ export const useGetRecordFilterDisplayValue = () => {
|
||||
}
|
||||
|
||||
const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow(
|
||||
recordFilter.relationTargetField?.id ?? recordFilter.fieldMetadataId,
|
||||
recordFilter.relationTargetFieldMetadataId ??
|
||||
recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
const fieldMetadataItemOptions = fieldMetadataItem.options;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType';
|
||||
import { type CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType';
|
||||
|
||||
export type ObjectFilterDropdownSubMenuFieldType =
|
||||
| CompositeFilterableFieldType
|
||||
| typeof RELATION_SUB_MENU_FIELD_TYPE;
|
||||
export type ObjectFilterDropdownSubMenuFieldType = CompositeFilterableFieldType;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type FilterableAndTSVectorFieldType,
|
||||
type ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
@@ -25,16 +24,7 @@ export type RecordFilter = {
|
||||
positionInRecordFilterGroup?: number | null;
|
||||
label: string;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
// Resolved at filter construction time (frontend mapping or chip edit) so
|
||||
// the GraphQL builder doesn't need a cross-object field lookup. Null for
|
||||
// non-relation filters; also null when the target field can no longer be
|
||||
// resolved (e.g. it was deleted).
|
||||
relationTargetField?: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
label: string;
|
||||
} | null;
|
||||
relationTargetFieldMetadataId?: string | null;
|
||||
rlsDynamicValue?: RLSDynamicValue | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import { hasInitializedCurrentRecordFieldsComponentFamilyState } from '@/views/s
|
||||
import { hasInitializedCurrentRecordFiltersComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFiltersComponentFamilyState';
|
||||
import { hasInitializedCurrentRecordSortsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordSortsComponentFamilyState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { getFilterableFields } from '@/views/utils/getFilterableFields';
|
||||
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
|
||||
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
|
||||
@@ -129,13 +128,11 @@ export const useLoadRecordIndexStates = () => {
|
||||
.map(mapViewFieldToRecordField)
|
||||
.filter(isDefined);
|
||||
|
||||
const allFilterableFields = getFilterableFields(objectMetadataItem);
|
||||
const flattenedFieldMetadataItems = store.get(
|
||||
flattenedFieldMetadataItemsSelector.atom,
|
||||
);
|
||||
const recordFilters = mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
allFilterableFields,
|
||||
flattenedFieldMetadataItems,
|
||||
);
|
||||
|
||||
@@ -145,7 +142,6 @@ export const useLoadRecordIndexStates = () => {
|
||||
|
||||
const contextStoreFilters = mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterableFieldMetadataItems,
|
||||
flattenedFieldMetadataItems,
|
||||
);
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/Gene
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useContext } from 'react';
|
||||
@@ -89,6 +91,8 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const handleFieldSelect = (
|
||||
selectedFieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
@@ -102,13 +106,31 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF
|
||||
setObjectFilterDropdownSubMenuFieldType(filterType);
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownIsSelectingCompositeField(true);
|
||||
} else {
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: selectedFieldMetadataItem.id,
|
||||
recordFilterId,
|
||||
});
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: selectedFieldMetadataItem.id,
|
||||
recordFilterId,
|
||||
});
|
||||
|
||||
// RELATION/SELECT leaf fields open a value picker keyed by the
|
||||
// source field id — push it on the focus stack so the picker's
|
||||
// keyboard hotkeys are active when it opens.
|
||||
if (
|
||||
selectedFieldMetadataItem.type === FieldMetadataType.RELATION ||
|
||||
selectedFieldMetadataItem.type === FieldMetadataType.SELECT
|
||||
) {
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: selectedFieldMetadataItem.id,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: selectedFieldMetadataItem.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
};
|
||||
|
||||
const selectableItemIdArray = filteredFieldMetadataItems.map(
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-f
|
||||
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { RELATION_SUB_MENU_FIELD_TYPE } from '@/object-record/object-filter-dropdown/constants/RelationSubMenuFieldType';
|
||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||
import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameBySubField';
|
||||
@@ -95,10 +94,7 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectS
|
||||
advancedFilterFieldSelectDropdownId,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(objectFilterDropdownSubMenuFieldType) ||
|
||||
objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE
|
||||
) {
|
||||
if (!isDefined(objectFilterDropdownSubMenuFieldType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,7 @@ export const RecordTableSettingsFilters = ({
|
||||
isWorkflowFindRecords={false}
|
||||
onUpdate={handleFilterUpdate}
|
||||
/>
|
||||
<RecordTableSettingsFiltersInitializeStateEffect
|
||||
view={view}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
<RecordTableSettingsFiltersInitializeStateEffect view={view} />
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
</RecordFilterGroupsComponentInstanceContext.Provider>
|
||||
</StyledFilterSettingsContainer>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useSetAdvancedFilterDropdownStates } from '@/object-record/advanced-filter/hooks/useSetAdvancedFilterDropdownAllRowsStates';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
@@ -7,7 +6,6 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { getFilterableFields } from '@/views/utils/getFilterableFields';
|
||||
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -15,12 +13,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordTableSettingsFiltersInitializeStateEffectProps = {
|
||||
view: View;
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
};
|
||||
|
||||
export const RecordTableSettingsFiltersInitializeStateEffect = ({
|
||||
view,
|
||||
objectMetadataItem,
|
||||
}: RecordTableSettingsFiltersInitializeStateEffectProps) => {
|
||||
const setCurrentRecordFilters = useSetAtomComponentState(
|
||||
currentRecordFiltersComponentState,
|
||||
@@ -60,10 +56,8 @@ export const RecordTableSettingsFiltersInitializeStateEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const filterableFields = getFilterableFields(objectMetadataItem);
|
||||
const recordFilters = mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterableFields,
|
||||
flattenedFieldMetadataItems,
|
||||
);
|
||||
|
||||
@@ -78,7 +72,6 @@ export const RecordTableSettingsFiltersInitializeStateEffect = ({
|
||||
setHasInitializedFilters(true);
|
||||
}, [
|
||||
view,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems,
|
||||
hasInitializedFilters,
|
||||
stateAlreadyHasFilters,
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
|
||||
label: mockFieldMetadataItem.label,
|
||||
type: getFilterTypeFromFieldType(mockFieldMetadataItem.type),
|
||||
subFieldName: null,
|
||||
relationTargetField: null,
|
||||
relationTargetFieldMetadataId: null,
|
||||
} satisfies RecordFilter,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type ViewFilter as GqlViewFilter } from '~/generated-metadata/graphql';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { getFilterableFields } from '@/views/utils/getFilterableFields';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
|
||||
export const useMapViewFiltersToFilters = () => {
|
||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
@@ -16,13 +12,7 @@ export const useMapViewFiltersToFilters = () => {
|
||||
const mapViewFiltersToRecordFilters = (
|
||||
viewFilters: ViewFilter[] | GqlViewFilter[],
|
||||
) => {
|
||||
const filterableFieldMetadataItems =
|
||||
getFilterableFields(objectMetadataItem);
|
||||
return mapViewFiltersToFilters(
|
||||
viewFilters,
|
||||
filterableFieldMetadataItems,
|
||||
flattenedFieldMetadataItems,
|
||||
);
|
||||
return mapViewFiltersToFilters(viewFilters, flattenedFieldMetadataItems);
|
||||
};
|
||||
|
||||
return { mapViewFiltersToRecordFilters };
|
||||
|
||||
@@ -67,7 +67,7 @@ export const useSetEditableFilterChipDropdownStates = () => {
|
||||
recordFilterId: recordFilter.id,
|
||||
}),
|
||||
}),
|
||||
recordFilter.relationTargetField?.id ?? null,
|
||||
recordFilter.relationTargetFieldMetadataId ?? null,
|
||||
);
|
||||
},
|
||||
[store, filterableFieldMetadataItems],
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('mapViewFiltersToFilters', () => {
|
||||
positionInRecordFilterGroup: undefined,
|
||||
recordFilterGroupId: undefined,
|
||||
subFieldName: undefined,
|
||||
relationTargetField: null,
|
||||
relationTargetFieldMetadataId: null,
|
||||
},
|
||||
];
|
||||
expect(
|
||||
|
||||
@@ -13,6 +13,7 @@ export const mapRecordFilterToViewFilter = (
|
||||
positionInViewFilterGroup: recordFilter.positionInRecordFilterGroup,
|
||||
viewFilterGroupId: recordFilter.recordFilterGroupId,
|
||||
subFieldName: recordFilter.subFieldName,
|
||||
relationTargetFieldMetadataId: recordFilter.relationTargetField?.id ?? null,
|
||||
relationTargetFieldMetadataId:
|
||||
recordFilter.relationTargetFieldMetadataId ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,25 +14,24 @@ import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
|
||||
export const mapViewFiltersToFilters = (
|
||||
viewFilters: ViewFilter[] | GqlViewFilter[],
|
||||
availableFieldMetadataItems: FieldMetadataItem[],
|
||||
allFieldMetadataItems: FieldMetadataItem[] = availableFieldMetadataItems,
|
||||
fieldMetadataItems: FieldMetadataItem[],
|
||||
): RecordFilter[] => {
|
||||
return viewFilters
|
||||
.map((viewFilter) => {
|
||||
const availableFieldMetadataItem = availableFieldMetadataItems.find(
|
||||
const sourceFieldMetadataItem = fieldMetadataItems.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === viewFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(availableFieldMetadataItem)) {
|
||||
// Todo: we we don't throw an error yet as we have race condition on view change
|
||||
if (!isDefined(sourceFieldMetadataItem)) {
|
||||
// Todo: we don't throw an error yet as we have race condition on view change
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const relationTargetFieldMetadataItem = isDefined(
|
||||
viewFilter.relationTargetFieldMetadataId,
|
||||
)
|
||||
? allFieldMetadataItems.find(
|
||||
? fieldMetadataItems.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === viewFilter.relationTargetFieldMetadataId,
|
||||
)
|
||||
@@ -40,13 +39,13 @@ export const mapViewFiltersToFilters = (
|
||||
|
||||
const filterType = isDefined(relationTargetFieldMetadataItem)
|
||||
? getFilterTypeFromFieldType(relationTargetFieldMetadataItem.type)
|
||||
: getFilterTypeFromFieldType(availableFieldMetadataItem.type);
|
||||
: getFilterTypeFromFieldType(sourceFieldMetadataItem.type);
|
||||
|
||||
const label = isSystemSearchVectorField(availableFieldMetadataItem.name)
|
||||
const label = isSystemSearchVectorField(sourceFieldMetadataItem.name)
|
||||
? 'Search'
|
||||
: isDefined(relationTargetFieldMetadataItem)
|
||||
? `${availableFieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}`
|
||||
: availableFieldMetadataItem.label;
|
||||
? `${sourceFieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}`
|
||||
: sourceFieldMetadataItem.label;
|
||||
|
||||
const operand = viewFilter.operand;
|
||||
|
||||
@@ -66,14 +65,8 @@ export const mapViewFiltersToFilters = (
|
||||
label,
|
||||
type: filterType,
|
||||
subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName,
|
||||
relationTargetField: isDefined(relationTargetFieldMetadataItem)
|
||||
? {
|
||||
id: relationTargetFieldMetadataItem.id,
|
||||
name: relationTargetFieldMetadataItem.name,
|
||||
type: relationTargetFieldMetadataItem.type,
|
||||
label: relationTargetFieldMetadataItem.label,
|
||||
}
|
||||
: null,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
} satisfies RecordFilter;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
@@ -222,34 +222,17 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
);
|
||||
}
|
||||
|
||||
const relationTargetField = isDefined(
|
||||
viewFilter.relationTargetFieldMetadataId,
|
||||
)
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: viewFilter.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
value: convertViewFilterValueToString(viewFilter.value),
|
||||
type: getFilterTypeFromFieldType(
|
||||
relationTargetField?.type ?? fieldMetadata.type,
|
||||
),
|
||||
type: getFilterTypeFromFieldType(fieldMetadata.type),
|
||||
operand: viewFilter.operand,
|
||||
recordFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName,
|
||||
relationTargetField: isDefined(relationTargetField)
|
||||
? {
|
||||
id: relationTargetField.id,
|
||||
name: relationTargetField.name,
|
||||
type: relationTargetField.type,
|
||||
label: relationTargetField.label,
|
||||
}
|
||||
: null,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -82,31 +82,16 @@ export class ViewQueryParamsService {
|
||||
|
||||
if (!field) return null;
|
||||
|
||||
const relationTargetField = isDefined(
|
||||
viewFilter.relationTargetFieldMetadataId,
|
||||
)
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: viewFilter.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
value: viewFilter.value ?? '',
|
||||
type: relationTargetField?.type ?? field.type,
|
||||
type: field.type,
|
||||
recordFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
operand: viewFilter.operand,
|
||||
subFieldName: viewFilter.subFieldName,
|
||||
relationTargetField: isDefined(relationTargetField)
|
||||
? {
|
||||
id: relationTargetField.id,
|
||||
name: relationTargetField.name,
|
||||
type: relationTargetField.type,
|
||||
label: relationTargetField.label,
|
||||
}
|
||||
: null,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
} as RecordFilter;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
type CompositeFieldSubFieldName,
|
||||
type FieldMetadataType,
|
||||
type FilterableAndTSVectorFieldType,
|
||||
type PartialFieldMetadataItem,
|
||||
RecordFilterGroupLogicalOperator,
|
||||
@@ -20,12 +19,7 @@ export type RecordFilter = {
|
||||
recordFilterGroupId?: string | null;
|
||||
operand: ViewFilterOperand;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
// Resolved at filter construction time so traversal doesn't need a
|
||||
// cross-object field lookup downstream.
|
||||
relationTargetField?:
|
||||
| { id: string; name: string; type: FieldMetadataType; label: string }
|
||||
| null
|
||||
| undefined;
|
||||
relationTargetFieldMetadataId?: string | null | undefined;
|
||||
};
|
||||
|
||||
export type RecordFilterGroup = {
|
||||
|
||||
@@ -89,25 +89,28 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
// Must run before the emptiness shortcut so an "is empty" filter on
|
||||
// `company.name` is evaluated against `Company.name`, not the FK column.
|
||||
// If `relationTargetField` is missing the filter is dropped, not fed
|
||||
// through the legacy relation-by-record path — that path parses `value`
|
||||
// as a UUID list and would silently mishandle a target-field value like
|
||||
// "Acme", risking broadening of destructive operations.
|
||||
// If the target field can't be resolved the filter is dropped rather than
|
||||
// fed to the per-type switch as a RELATION (which would parse `value` as
|
||||
// a UUID list and silently mishandle target-field values like "Acme").
|
||||
if (
|
||||
correspondingFieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
isDefined(recordFilter.relationTargetField)
|
||||
isDefined(recordFilter.relationTargetFieldMetadataId)
|
||||
) {
|
||||
const targetField = recordFilter.relationTargetField;
|
||||
const targetFieldMetadataItem = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.relationTargetFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetFieldMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const innerFilter = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
recordFilter: {
|
||||
...recordFilter,
|
||||
fieldMetadataId: targetField.id,
|
||||
relationTargetField: null,
|
||||
fieldMetadataId: targetFieldMetadataItem.id,
|
||||
relationTargetFieldMetadataId: null,
|
||||
},
|
||||
// Inject the target so the recursive lookup succeeds even when the
|
||||
// caller only supplied the current object's fields.
|
||||
fieldMetadataItems: [...fieldMetadataItems, targetField],
|
||||
fieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user