mirror of
https://github.com/twentyhq/twenty.git
synced 2026-05-25 00:45:27 -04:00
refactor(twenty-front): move sub-menu type / constant out of state, extract relation sub-menu
Two review nits: 1. `RELATION_SUB_MENU_FIELD_TYPE` and `ObjectFilterDropdownSubMenuFieldType` lived inside the state file. They aren't state — moved to `object-filter-dropdown/constants/` and `record-filter/types/` respectively, alongside the rest of the constants and types. 2. The relation sub-menu branch in `AdvancedFilterSubFieldSelectMenu` computed `targetObjectMetadataId` with an empty-string sentinel so the always-running `useFilterableFieldMetadataItems` hook could accept it. Fragile (the `''` sentinel is unnamed, untyped, and relies on the selector silently returning `[]`). Extracted the relation branch into its own `AdvancedFilterRelationSubMenu` component that only mounts when actually in relation mode — the hook now always receives a real object id, no sentinel. Also: turned `isManyToOneRelationField` into a generic type guard so callers can read `field.relation.targetObjectMetadata.id` after the check without a non-null assertion.
This commit is contained in:
@@ -5,6 +5,8 @@ type FieldWithRelation = {
|
||||
relation?: { type: RelationType } | null;
|
||||
};
|
||||
|
||||
export const isManyToOneRelationField = (field: FieldWithRelation): boolean =>
|
||||
export const isManyToOneRelationField = <T extends FieldWithRelation>(
|
||||
field: T,
|
||||
): field is T & { relation: NonNullable<T['relation']> } =>
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.relation?.type === RelationType.MANY_TO_ONE;
|
||||
|
||||
@@ -16,11 +16,9 @@ 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 {
|
||||
type ObjectFilterDropdownSubMenuFieldType,
|
||||
RELATION_SUB_MENU_FIELD_TYPE,
|
||||
objectFilterDropdownSubMenuFieldTypeComponentState,
|
||||
} from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
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 { 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';
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -5,19 +5,17 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
|
||||
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 {
|
||||
RELATION_SUB_MENU_FIELD_TYPE,
|
||||
objectFilterDropdownSubMenuFieldTypeComponentState,
|
||||
} from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { ICON_NAME_BY_SUB_FIELD } from '@/object-record/record-filter/constants/IconNameBySubField';
|
||||
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
|
||||
import { areCompositeTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable';
|
||||
import { isCompositeTypeNonFilterableByAnySubField } from '@/object-record/record-filter/utils/isCompositeTypeNonFilterableByAnySubField';
|
||||
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
|
||||
@@ -62,18 +60,6 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
const { selectFieldUsedInAdvancedFilterDropdown } =
|
||||
useSelectFieldUsedInAdvancedFilterDropdown();
|
||||
|
||||
const isRelationSubMenu =
|
||||
objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE &&
|
||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||
isManyToOneRelationField(fieldMetadataItemUsedInDropdown);
|
||||
|
||||
const targetObjectMetadataId = isRelationSubMenu
|
||||
? (fieldMetadataItemUsedInDropdown?.relation?.targetObjectMetadata.id ?? '')
|
||||
: '';
|
||||
|
||||
const { filterableFieldMetadataItems: relationTargetFields } =
|
||||
useFilterableFieldMetadataItems(targetObjectMetadataId);
|
||||
|
||||
const handleSelectFilter = ({
|
||||
fieldMetadataItem,
|
||||
subFieldName,
|
||||
@@ -113,57 +99,21 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isRelationSubMenu && isDefined(fieldMetadataItemUsedInDropdown)) {
|
||||
const fieldLabel = fieldMetadataItemUsedInDropdown.label;
|
||||
const selectableItemIdArray = relationTargetFields.map((field) => field.id);
|
||||
|
||||
if (
|
||||
objectFilterDropdownSubMenuFieldType === RELATION_SUB_MENU_FIELD_TYPE &&
|
||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||
isManyToOneRelationField(fieldMetadataItemUsedInDropdown)
|
||||
) {
|
||||
return (
|
||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={handleSubMenuBack}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{fieldLabel}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
focusId={advancedFilterFieldSelectDropdownId}
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
|
||||
>
|
||||
{relationTargetFields.map((targetField, index) => (
|
||||
<SelectableListItem
|
||||
itemId={targetField.id}
|
||||
key={`select-filter-relation-${index}`}
|
||||
onEnter={() => {
|
||||
handleSelectFilter({
|
||||
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
|
||||
relationTargetFieldMetadataItem: targetField,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === targetField.id}
|
||||
key={`select-filter-relation-${index}`}
|
||||
testId={`select-filter-relation-${index}`}
|
||||
onClick={() => {
|
||||
handleSelectFilter({
|
||||
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
|
||||
relationTargetFieldMetadataItem: targetField,
|
||||
});
|
||||
}}
|
||||
text={targetField.label}
|
||||
LeftIcon={getIcon(targetField.icon)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
<AdvancedFilterRelationSubMenu
|
||||
recordFilterId={recordFilterId}
|
||||
relationFieldMetadataItem={fieldMetadataItemUsedInDropdown}
|
||||
targetObjectMetadataId={
|
||||
fieldMetadataItemUsedInDropdown.relation.targetObjectMetadata.id
|
||||
}
|
||||
onBack={handleSubMenuBack}
|
||||
onSelectTargetField={handleSelectFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const RELATION_SUB_MENU_FIELD_TYPE = 'RELATION' as const;
|
||||
@@ -1,13 +1,7 @@
|
||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||
import { type CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType';
|
||||
import { type ObjectFilterDropdownSubMenuFieldType } from '@/object-record/record-filter/types/ObjectFilterDropdownSubMenuFieldType';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
export const RELATION_SUB_MENU_FIELD_TYPE = 'RELATION' as const;
|
||||
|
||||
export type ObjectFilterDropdownSubMenuFieldType =
|
||||
| CompositeFilterableFieldType
|
||||
| typeof RELATION_SUB_MENU_FIELD_TYPE;
|
||||
|
||||
export const objectFilterDropdownSubMenuFieldTypeComponentState =
|
||||
createAtomComponentState<ObjectFilterDropdownSubMenuFieldType | null>({
|
||||
key: 'objectFilterDropdownSubMenuFieldTypeComponentState',
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
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;
|
||||
@@ -13,10 +13,8 @@ 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,
|
||||
objectFilterDropdownSubMenuFieldTypeComponentState,
|
||||
} from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
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';
|
||||
import { areCompositeTypeSubFieldsFilterable } from '@/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable';
|
||||
|
||||
Reference in New Issue
Block a user