Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46b0c46beb | ||
|
|
52726ce943 | ||
|
|
312bb37c40 | ||
|
|
4f707bb9f1 | ||
|
|
e0e36a8f00 | ||
|
|
4a78b092a2 | ||
|
|
0a18423149 | ||
|
|
e3638925a2 | ||
|
|
88d0476da0 | ||
|
|
203f72116a | ||
|
|
a23ab663bc | ||
|
|
2bd03cab91 | ||
|
|
50bd2a78a7 | ||
|
|
54894044b2 | ||
|
|
7158fab9fa | ||
|
|
858ae62b67 | ||
|
|
41239c9f0e | ||
|
|
49633515d2 | ||
|
|
90a1381c86 | ||
|
|
d947fed809 | ||
|
|
d39a2d00c1 | ||
|
|
68d513662c | ||
|
|
24884e9a27 | ||
|
|
3040e75e97 | ||
|
|
4a37f654f8 | ||
|
|
2dc23464e7 | ||
|
|
06f8c8b5a7 | ||
|
|
fd1b51ac03 | ||
|
|
b805b962f1 | ||
|
|
96a6a08a51 | ||
|
|
0153ea3887 | ||
|
|
f3211f1d52 | ||
|
|
e97f466fe6 | ||
|
|
6076a79bda | ||
|
|
1f4225fb83 | ||
|
|
36aadfa649 | ||
|
|
9529764c94 | ||
|
|
a90a6e3bc6 | ||
|
|
8461ddbadd |
@@ -771,6 +771,7 @@ type ViewFilter {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
viewId: UUID!
|
||||
workspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
@@ -3416,6 +3417,7 @@ input CreateViewFilterInput {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
viewId: UUID!
|
||||
}
|
||||
|
||||
@@ -3434,6 +3436,7 @@ input UpdateViewFilterInputUpdates {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
}
|
||||
|
||||
input DeleteViewFilterInput {
|
||||
@@ -3522,6 +3525,7 @@ input UpsertViewWidgetViewFilterInput {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterGroupInput {
|
||||
|
||||
@@ -523,6 +523,7 @@ export interface ViewFilter {
|
||||
viewFilterGroupId?: Scalars['UUID']
|
||||
positionInViewFilterGroup?: Scalars['Float']
|
||||
subFieldName?: Scalars['String']
|
||||
relationTargetFieldMetadataId?: Scalars['UUID']
|
||||
viewId: Scalars['UUID']
|
||||
workspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
@@ -3451,6 +3452,7 @@ export interface ViewFilterGenqlSelection{
|
||||
viewFilterGroupId?: boolean | number
|
||||
positionInViewFilterGroup?: boolean | number
|
||||
subFieldName?: boolean | number
|
||||
relationTargetFieldMetadataId?: boolean | number
|
||||
viewId?: boolean | number
|
||||
workspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
@@ -5973,7 +5975,7 @@ export interface CreateViewFilterGroupInput {id?: (Scalars['UUID'] | null),paren
|
||||
|
||||
export interface UpdateViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null),viewId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface CreateViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),viewId: Scalars['UUID']}
|
||||
export interface CreateViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null),viewId: Scalars['UUID']}
|
||||
|
||||
export interface UpdateViewFilterInput {
|
||||
/** The id of the view filter to update */
|
||||
@@ -5981,7 +5983,7 @@ id: Scalars['UUID'],
|
||||
/** The view filter to update */
|
||||
update: UpdateViewFilterInputUpdates}
|
||||
|
||||
export interface UpdateViewFilterInputUpdates {fieldMetadataId?: (Scalars['UUID'] | null),operand?: (ViewFilterOperand | null),value?: (Scalars['JSON'] | null),viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
|
||||
export interface UpdateViewFilterInputUpdates {fieldMetadataId?: (Scalars['UUID'] | null),operand?: (ViewFilterOperand | null),value?: (Scalars['JSON'] | null),viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface DeleteViewFilterInput {
|
||||
/** The id of the view filter to delete. */
|
||||
@@ -6013,7 +6015,7 @@ viewFieldId?: (Scalars['UUID'] | null),
|
||||
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
|
||||
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
|
||||
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
|
||||
@@ -1521,6 +1521,9 @@ export default {
|
||||
"subFieldName": [
|
||||
1
|
||||
],
|
||||
"relationTargetFieldMetadataId": [
|
||||
3
|
||||
],
|
||||
"viewId": [
|
||||
3
|
||||
],
|
||||
@@ -8929,6 +8932,9 @@ export default {
|
||||
"subFieldName": [
|
||||
1
|
||||
],
|
||||
"relationTargetFieldMetadataId": [
|
||||
3
|
||||
],
|
||||
"viewId": [
|
||||
3
|
||||
],
|
||||
@@ -8966,6 +8972,9 @@ export default {
|
||||
"subFieldName": [
|
||||
1
|
||||
],
|
||||
"relationTargetFieldMetadataId": [
|
||||
3
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
@@ -9154,6 +9163,9 @@ export default {
|
||||
"subFieldName": [
|
||||
1
|
||||
],
|
||||
"relationTargetFieldMetadataId": [
|
||||
3
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
+5
@@ -89,12 +89,17 @@ export const buildHeadlessCommandContextApi = ({
|
||||
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
|
||||
: systemTimeZone;
|
||||
|
||||
const flattenedFieldMetadataItems = objectMetadataItems.flatMap(
|
||||
(objectMetadataItem) => objectMetadataItem.fields,
|
||||
);
|
||||
|
||||
const graphqlFilter = isDefined(objectMetadataItem)
|
||||
? computeContextStoreFilters({
|
||||
contextStoreTargetedRecordsRule: targetedRecordsRule,
|
||||
contextStoreFilters: filters,
|
||||
contextStoreFilterGroups: filterGroups,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems,
|
||||
filterValueDependencies: {
|
||||
currentWorkspaceMemberId: currentWorkspaceMember?.id,
|
||||
timeZone: userTimezone,
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
|
||||
contextStoreFilters,
|
||||
contextStoreFilterGroups,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems: allFieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
contextStoreAnyFieldFilterValue,
|
||||
});
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ describe('computeContextStoreFilters', () => {
|
||||
contextStoreFilters: [],
|
||||
contextStoreFilterGroups: [],
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
|
||||
filterValueDependencies: mockFilterValueDependencies,
|
||||
contextStoreAnyFieldFilterValue: '',
|
||||
});
|
||||
@@ -73,6 +74,7 @@ describe('computeContextStoreFilters', () => {
|
||||
contextStoreFilters,
|
||||
contextStoreFilterGroups: [],
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
flattenedFieldMetadataItems: personObjectMetadataItem.fields,
|
||||
filterValueDependencies: mockFilterValueDependencies,
|
||||
contextStoreAnyFieldFilterValue: '',
|
||||
});
|
||||
|
||||
+12
-2
@@ -1,7 +1,9 @@
|
||||
import { type ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||
import {
|
||||
type RecordFilterValueDependencies,
|
||||
@@ -17,6 +19,7 @@ type ComputeContextStoreFiltersProps = {
|
||||
contextStoreFilters: RecordFilter[];
|
||||
contextStoreFilterGroups: RecordFilterGroup[];
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
flattenedFieldMetadataItems: FieldMetadataItem[];
|
||||
filterValueDependencies: RecordFilterValueDependencies;
|
||||
contextStoreAnyFieldFilterValue: string;
|
||||
};
|
||||
@@ -26,6 +29,7 @@ export const computeContextStoreFilters = ({
|
||||
contextStoreFilters,
|
||||
contextStoreFilterGroups,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
contextStoreAnyFieldFilterValue,
|
||||
}: ComputeContextStoreFiltersProps) => {
|
||||
@@ -37,12 +41,18 @@ export const computeContextStoreFilters = ({
|
||||
fields: objectMetadataItem.fields,
|
||||
});
|
||||
|
||||
const fields = augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem?.fields ?? [],
|
||||
recordFilters: contextStoreFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
});
|
||||
|
||||
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
|
||||
queryFilter = makeAndFilterVariables([
|
||||
recordGqlFilterForAnyFieldFilter,
|
||||
computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
fields: objectMetadataItem?.fields ?? [],
|
||||
fields,
|
||||
recordFilters: contextStoreFilters,
|
||||
recordFilterGroups: contextStoreFilterGroups,
|
||||
}),
|
||||
@@ -71,7 +81,7 @@ export const computeContextStoreFilters = ({
|
||||
},
|
||||
computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
fields: objectMetadataItem?.fields ?? [],
|
||||
fields,
|
||||
recordFilters: contextStoreFilters,
|
||||
recordFilterGroups: contextStoreFilterGroups,
|
||||
}),
|
||||
|
||||
+6
-6
@@ -1,6 +1,10 @@
|
||||
import { SORTABLE_FIELD_METADATA_TYPES } from '@/object-metadata/constants/SortableFieldMetadataTypes';
|
||||
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type RelationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type SortableFieldInput = {
|
||||
isSystem?: boolean | null;
|
||||
@@ -17,13 +21,9 @@ export const filterSortableFieldMetadataItems = (field: SortableFieldInput) => {
|
||||
field.type,
|
||||
);
|
||||
|
||||
const isRelationFieldSortable =
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.relation?.type === RelationType.MANY_TO_ONE;
|
||||
|
||||
return (
|
||||
!isHiddenSystemField(field) &&
|
||||
isFieldActive &&
|
||||
(isFieldTypeSortable || isRelationFieldSortable)
|
||||
(isFieldTypeSortable || isManyToOneRelationField(field))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type FieldWithRelation = {
|
||||
type: FieldMetadataType;
|
||||
relation?: { type: RelationType } | null;
|
||||
};
|
||||
|
||||
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;
|
||||
+53
-52
@@ -1,13 +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 { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
||||
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
|
||||
import { useApplyAdvancedFilterCompositeSubField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterCompositeSubField';
|
||||
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';
|
||||
@@ -20,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(
|
||||
@@ -54,21 +52,20 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
const { closeAdvancedFilterFieldSelectDropdown } =
|
||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||
|
||||
const { selectFieldUsedInAdvancedFilterDropdown } =
|
||||
useSelectFieldUsedInAdvancedFilterDropdown();
|
||||
const { applyAdvancedFilterCompositeSubField } =
|
||||
useApplyAdvancedFilterCompositeSubField();
|
||||
|
||||
const handleSelectFilter = (
|
||||
selectedFieldMetadataItem: FieldMetadataItem | null | undefined,
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined,
|
||||
) => {
|
||||
if (!isDefined(selectedFieldMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: selectedFieldMetadataItem.id,
|
||||
const handleSelectFilter = ({
|
||||
fieldMetadataItem,
|
||||
subFieldName,
|
||||
}: {
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
subFieldName?: CompositeFieldSubFieldName | null;
|
||||
}) => {
|
||||
applyAdvancedFilterCompositeSubField({
|
||||
sourceFieldMetadataItem: fieldMetadataItem,
|
||||
subFieldName: subFieldName ?? null,
|
||||
recordFilterId,
|
||||
subFieldName,
|
||||
});
|
||||
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
@@ -132,36 +129,42 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
selectableItemIdArray={selectableItemIdArray}
|
||||
selectableListInstanceId={advancedFilterFieldSelectDropdownId}
|
||||
>
|
||||
{compositeFieldTypeIsFilterableByAnySubField && (
|
||||
<SelectableListItem
|
||||
itemId="-1"
|
||||
key={`select-filter-${-1}`}
|
||||
onEnter={() => {
|
||||
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
{compositeFieldTypeIsFilterableByAnySubField &&
|
||||
isDefined(fieldMetadataItemUsedInDropdown) && (
|
||||
<SelectableListItem
|
||||
itemId="-1"
|
||||
key={`select-filter-${-1}`}
|
||||
testId={`select-filter-${-1}`}
|
||||
focused={selectedItemId === '-1'}
|
||||
onClick={() => {
|
||||
handleSelectFilter(fieldMetadataItemUsedInDropdown);
|
||||
onEnter={() => {
|
||||
handleSelectFilter({
|
||||
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
|
||||
});
|
||||
}}
|
||||
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)}
|
||||
text={t`Any ${fieldLabel} field`}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
>
|
||||
<MenuItem
|
||||
key={`select-filter-${-1}`}
|
||||
testId={`select-filter-${-1}`}
|
||||
focused={selectedItemId === '-1'}
|
||||
onClick={() => {
|
||||
handleSelectFilter({
|
||||
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
|
||||
});
|
||||
}}
|
||||
LeftIcon={getIcon(fieldMetadataItemUsedInDropdown.icon)}
|
||||
text={t`Any ${fieldLabel} field`}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
{subFieldsAreFilterable &&
|
||||
isDefined(fieldMetadataItemUsedInDropdown) &&
|
||||
subFieldNames.map((subFieldName, index) => (
|
||||
<SelectableListItem
|
||||
itemId={subFieldName}
|
||||
key={`select-filter-${index}`}
|
||||
onEnter={() => {
|
||||
handleSelectFilter(
|
||||
fieldMetadataItemUsedInDropdown,
|
||||
handleSelectFilter({
|
||||
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
|
||||
subFieldName,
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
@@ -169,12 +172,10 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
key={`select-filter-${index}`}
|
||||
testId={`select-filter-${index}`}
|
||||
onClick={() => {
|
||||
if (isDefined(fieldMetadataItemUsedInDropdown)) {
|
||||
handleSelectFilter(
|
||||
fieldMetadataItemUsedInDropdown,
|
||||
subFieldName,
|
||||
);
|
||||
}
|
||||
handleSelectFilter({
|
||||
fieldMetadataItem: fieldMetadataItemUsedInDropdown,
|
||||
subFieldName,
|
||||
});
|
||||
}}
|
||||
text={getCompositeSubFieldLabel(
|
||||
objectFilterDropdownSubMenuFieldType,
|
||||
@@ -182,7 +183,7 @@ export const AdvancedFilterSubFieldSelectMenu = ({
|
||||
)}
|
||||
LeftIcon={getIcon(
|
||||
ICON_NAME_BY_SUB_FIELD[subFieldName] ??
|
||||
fieldMetadataItemUsedInDropdown?.icon,
|
||||
fieldMetadataItemUsedInDropdown.icon,
|
||||
)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
+29
-12
@@ -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,31 @@ type AdvancedFilterFieldSelectDropdownContentProps = {
|
||||
export const AdvancedFilterFieldSelectDropdownContent = ({
|
||||
recordFilterId,
|
||||
}: AdvancedFilterFieldSelectDropdownContentProps) => {
|
||||
const [objectFilterDropdownIsSelectingCompositeField] = useAtomComponentState(
|
||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||
);
|
||||
const objectFilterDropdownIsSelectingCompositeField =
|
||||
useAtomComponentStateValue(
|
||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||
);
|
||||
|
||||
const shouldShowCompositeSelectionSubMenu =
|
||||
objectFilterDropdownIsSelectingCompositeField;
|
||||
const objectFilterDropdownIsSelectingRelationTargetField =
|
||||
useAtomComponentStateValue(
|
||||
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
|
||||
);
|
||||
|
||||
return shouldShowCompositeSelectionSubMenu ? (
|
||||
<AdvancedFilterSubFieldSelectMenu recordFilterId={recordFilterId} />
|
||||
) : (
|
||||
<AdvancedFilterFieldSelectMenu recordFilterId={recordFilterId} />
|
||||
);
|
||||
if (objectFilterDropdownIsSelectingRelationTargetField) {
|
||||
return (
|
||||
<AdvancedFilterRelationTargetFieldSelectMenu
|
||||
recordFilterId={recordFilterId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (objectFilterDropdownIsSelectingCompositeField) {
|
||||
return (
|
||||
<AdvancedFilterCompositeSubFieldSelectMenu
|
||||
recordFilterId={recordFilterId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AdvancedFilterFieldSelectMenu recordFilterId={recordFilterId} />;
|
||||
};
|
||||
|
||||
+45
-12
@@ -11,17 +11,20 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { AdvancedFilterFieldSelectSearchInput } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectSearchInput';
|
||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
||||
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
|
||||
import { useApplyAdvancedFilterSourceField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterSourceField';
|
||||
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
|
||||
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 { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
|
||||
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
|
||||
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';
|
||||
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
|
||||
import { usePushFocusForLeafFieldValuePicker } from '@/object-record/advanced-filter/hooks/usePushFocusForLeafFieldValuePicker';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
@@ -87,8 +90,8 @@ export const AdvancedFilterFieldSelectMenu = ({
|
||||
advancedFilterFieldSelectDropdownId,
|
||||
);
|
||||
|
||||
const { selectFieldUsedInAdvancedFilterDropdown } =
|
||||
useSelectFieldUsedInAdvancedFilterDropdown();
|
||||
const { applyAdvancedFilterSourceField } =
|
||||
useApplyAdvancedFilterSourceField();
|
||||
|
||||
const [, setObjectFilterDropdownSubMenuFieldType] = useAtomComponentState(
|
||||
objectFilterDropdownSubMenuFieldTypeComponentState,
|
||||
@@ -99,10 +102,18 @@ export const AdvancedFilterFieldSelectMenu = ({
|
||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||
);
|
||||
|
||||
const [, setObjectFilterDropdownIsSelectingRelationTargetField] =
|
||||
useAtomComponentState(
|
||||
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
|
||||
);
|
||||
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const { pushFocusForLeafFieldValuePicker } =
|
||||
usePushFocusForLeafFieldValuePicker();
|
||||
|
||||
const handleFieldMetadataItemSelect = (
|
||||
selectedFieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
@@ -112,19 +123,41 @@ export const AdvancedFilterFieldSelectMenu = ({
|
||||
selectedFieldMetadataItem.type,
|
||||
);
|
||||
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: selectedFieldMetadataItem.id,
|
||||
const isRelationTraversalField = isManyToOneRelationField(
|
||||
selectedFieldMetadataItem,
|
||||
);
|
||||
|
||||
const compositeSubMenuFieldType =
|
||||
!isRelationTraversalField && isCompositeFilterableFieldType(filterType)
|
||||
? filterType
|
||||
: null;
|
||||
|
||||
// For sub-menu paths (composite or relation traversal) we only stage
|
||||
// the source field in the dropdown state and open the sub-menu —
|
||||
// upsertRecordFilter happens in the sub-menu's hook once the user
|
||||
// makes the final choice. Otherwise navigating back from the sub-menu
|
||||
// would leave an orphan partial filter in the chip list.
|
||||
if (isRelationTraversalField) {
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownIsSelectingRelationTargetField(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (compositeSubMenuFieldType !== null) {
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownSubMenuFieldType(compositeSubMenuFieldType);
|
||||
setObjectFilterDropdownIsSelectingCompositeField(true);
|
||||
return;
|
||||
}
|
||||
|
||||
applyAdvancedFilterSourceField({
|
||||
sourceFieldMetadataItem: selectedFieldMetadataItem,
|
||||
recordFilterId,
|
||||
});
|
||||
|
||||
if (isCompositeFilterableFieldType(filterType)) {
|
||||
setObjectFilterDropdownSubMenuFieldType(filterType);
|
||||
pushFocusForLeafFieldValuePicker(selectedFieldMetadataItem);
|
||||
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownIsSelectingCompositeField(true);
|
||||
} else {
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
}
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
};
|
||||
|
||||
const shouldShowVisibleFields = visibleFieldMetadataItems.length > 0;
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
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 { useApplyAdvancedFilterRelationTargetField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterRelationTargetField';
|
||||
import { usePushFocusForLeafFieldValuePicker } from '@/object-record/advanced-filter/hooks/usePushFocusForLeafFieldValuePicker';
|
||||
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 { 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 setObjectFilterDropdownIsSelectingRelationTargetField =
|
||||
useSetAtomComponentState(
|
||||
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
|
||||
);
|
||||
|
||||
const { closeAdvancedFilterFieldSelectDropdown } =
|
||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||
|
||||
const { applyAdvancedFilterRelationTargetField } =
|
||||
useApplyAdvancedFilterRelationTargetField();
|
||||
|
||||
const { pushFocusForLeafFieldValuePicker } =
|
||||
usePushFocusForLeafFieldValuePicker();
|
||||
|
||||
const { advancedFilterFieldSelectDropdownId } =
|
||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
advancedFilterFieldSelectDropdownId,
|
||||
);
|
||||
|
||||
const targetObjectMetadataId =
|
||||
isDefined(sourceFieldMetadataItem) &&
|
||||
isManyToOneRelationField(sourceFieldMetadataItem)
|
||||
? sourceFieldMetadataItem.relation.targetObjectMetadata.id
|
||||
: null;
|
||||
|
||||
const { filterableFieldMetadataItems: allTargetFields } =
|
||||
useFilterableFieldMetadataItems(targetObjectMetadataId ?? '');
|
||||
|
||||
// The backend supports a single hop only. Exclude many-to-one relations
|
||||
// from the target list so the user can't compose multi-hop traversals
|
||||
// (e.g. Person → Company → ParentCompany) that the dispatcher would
|
||||
// collapse back to a filter-by-id on the intermediate relation.
|
||||
const relationTargetFields = allTargetFields.filter(
|
||||
(field) => !isManyToOneRelationField(field),
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(sourceFieldMetadataItem) ||
|
||||
!isManyToOneRelationField(sourceFieldMetadataItem)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSubMenuBack = () => {
|
||||
setObjectFilterDropdownIsSelectingRelationTargetField(false);
|
||||
};
|
||||
|
||||
const handleSelectTargetField = (
|
||||
relationTargetFieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
applyAdvancedFilterRelationTargetField({
|
||||
sourceFieldMetadataItem,
|
||||
relationTargetFieldMetadataItem,
|
||||
recordFilterId,
|
||||
});
|
||||
|
||||
pushFocusForLeafFieldValuePicker(relationTargetFieldMetadataItem);
|
||||
|
||||
setObjectFilterDropdownIsSelectingRelationTargetField(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>
|
||||
);
|
||||
};
|
||||
+10
@@ -6,6 +6,7 @@ import { shouldShowFilterTextInput } from '@/object-record/advanced-filter/utils
|
||||
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 { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
@@ -55,6 +56,12 @@ export const AdvancedFilterValueInput = ({
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const setRelationTargetFieldMetadataIdUsedInDropdown =
|
||||
useSetAtomComponentState(
|
||||
relationTargetFieldMetadataIdUsedInDropdownComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
dropdownId,
|
||||
@@ -74,6 +81,9 @@ export const AdvancedFilterValueInput = ({
|
||||
const handleFilterValueDropdownOpen = () => {
|
||||
setObjectFilterDropdownCurrentRecordFilter(recordFilter);
|
||||
setFieldMetadataItemIdUsedInDropdown(recordFilter.fieldMetadataId);
|
||||
setRelationTargetFieldMetadataIdUsedInDropdown(
|
||||
recordFilter.relationTargetFieldMetadataId ?? null,
|
||||
);
|
||||
};
|
||||
|
||||
const filterType = recordFilter.type;
|
||||
|
||||
+31
-71
@@ -1,31 +1,30 @@
|
||||
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
|
||||
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 { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
|
||||
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';
|
||||
|
||||
type SelectFilterParams = {
|
||||
fieldMetadataItemId: string;
|
||||
type ApplyAdvancedFilterCompositeSubFieldParams = {
|
||||
sourceFieldMetadataItem: FieldMetadataItem;
|
||||
subFieldName: CompositeFieldSubFieldName | null;
|
||||
recordFilterId: string;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
};
|
||||
|
||||
export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
// Creates an advanced filter from a composite source field together with
|
||||
// the user's chosen sub-field (e.g. address city, name first name). The
|
||||
// filter type is still driven by the source field's composite type.
|
||||
export const useApplyAdvancedFilterCompositeSubField = () => {
|
||||
const setSelectedOperandInDropdown = useSetAtomComponentState(
|
||||
selectedOperandInDropdownComponentState,
|
||||
);
|
||||
@@ -38,54 +37,34 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
objectFilterDropdownSearchInputComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const { getFieldMetadataItemByIdOrThrow } =
|
||||
useGetFieldMetadataItemByIdOrThrow();
|
||||
|
||||
const setSubFieldNameUsedInDropdown = useSetAtomComponentState(
|
||||
subFieldNameUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setRelationTargetFieldMetadataIdUsedInDropdown =
|
||||
useSetAtomComponentState(
|
||||
relationTargetFieldMetadataIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
const { getInitialFilterValue } = useGetInitialFilterValue();
|
||||
|
||||
const selectFieldUsedInAdvancedFilterDropdown = ({
|
||||
fieldMetadataItemId,
|
||||
recordFilterId,
|
||||
const applyAdvancedFilterCompositeSubField = ({
|
||||
sourceFieldMetadataItem,
|
||||
subFieldName,
|
||||
}: SelectFilterParams) => {
|
||||
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId);
|
||||
recordFilterId,
|
||||
}: ApplyAdvancedFilterCompositeSubFieldParams) => {
|
||||
setFieldMetadataItemIdUsedInDropdown(sourceFieldMetadataItem.id);
|
||||
|
||||
const { fieldMetadataItem } =
|
||||
getFieldMetadataItemByIdOrThrow(fieldMetadataItemId);
|
||||
|
||||
if (!isDefined(fieldMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === 'RELATION' ||
|
||||
fieldMetadataItem.type === 'SELECT'
|
||||
) {
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: fieldMetadataItem.id,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: fieldMetadataItem.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
const filterType = getFilterTypeFromFieldType(sourceFieldMetadataItem.type);
|
||||
|
||||
const firstOperand = getRecordFilterOperands({
|
||||
filterType,
|
||||
@@ -109,30 +88,9 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
(recordFilter) => recordFilter.id === recordFilterId,
|
||||
);
|
||||
|
||||
const isCompositeFilterOnAnySubField =
|
||||
isCompositeFieldType(filterType) && !isDefined(subFieldName);
|
||||
const compositeFilterNonFilterableByAnySubField =
|
||||
isCompositeTypeNonFilterableByAnySubField(filterType);
|
||||
|
||||
let subFieldNameForNonFilterableWithAny:
|
||||
| CompositeFieldSubFieldName
|
||||
| undefined
|
||||
| null = subFieldName;
|
||||
|
||||
if (
|
||||
isCompositeFilterOnAnySubField &&
|
||||
compositeFilterNonFilterableByAnySubField
|
||||
) {
|
||||
subFieldNameForNonFilterableWithAny =
|
||||
getDefaultSubFieldNameForCompositeFilterableFieldType(filterType);
|
||||
}
|
||||
|
||||
const subFieldNameToUse =
|
||||
subFieldName ?? subFieldNameForNonFilterableWithAny;
|
||||
|
||||
const newAdvancedFilter = {
|
||||
id: recordFilterId,
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
fieldMetadataId: sourceFieldMetadataItem.id,
|
||||
displayValue,
|
||||
operand: firstOperand,
|
||||
value,
|
||||
@@ -140,11 +98,13 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
positionInRecordFilterGroup:
|
||||
existingRecordFilter?.positionInRecordFilterGroup,
|
||||
type: filterType,
|
||||
label: fieldMetadataItem.label,
|
||||
subFieldName: subFieldNameToUse,
|
||||
label: sourceFieldMetadataItem.label,
|
||||
subFieldName,
|
||||
relationTargetFieldMetadataId: null,
|
||||
} satisfies RecordFilter;
|
||||
|
||||
setSubFieldNameUsedInDropdown(subFieldNameToUse);
|
||||
setSubFieldNameUsedInDropdown(subFieldName);
|
||||
setRelationTargetFieldMetadataIdUsedInDropdown(null);
|
||||
|
||||
setObjectFilterDropdownSearchInput('');
|
||||
|
||||
@@ -153,6 +113,6 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
selectFieldUsedInAdvancedFilterDropdown,
|
||||
applyAdvancedFilterCompositeSubField,
|
||||
};
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
|
||||
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 { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
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';
|
||||
|
||||
type ApplyAdvancedFilterRelationTargetFieldParams = {
|
||||
sourceFieldMetadataItem: FieldMetadataItem;
|
||||
relationTargetFieldMetadataItem: FieldMetadataItem;
|
||||
recordFilterId: string;
|
||||
};
|
||||
|
||||
// Creates an advanced filter that traverses a relation: the source is the
|
||||
// relation field, the filter operates on a target field of the related
|
||||
// object. Filter type comes from the target field; the label combines
|
||||
// both labels so the chip reads "Source → Target".
|
||||
export const useApplyAdvancedFilterRelationTargetField = () => {
|
||||
const setSelectedOperandInDropdown = useSetAtomComponentState(
|
||||
selectedOperandInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownSearchInput = useSetAtomComponentState(
|
||||
objectFilterDropdownSearchInputComponentState,
|
||||
);
|
||||
|
||||
const setSubFieldNameUsedInDropdown = useSetAtomComponentState(
|
||||
subFieldNameUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setRelationTargetFieldMetadataIdUsedInDropdown =
|
||||
useSetAtomComponentState(
|
||||
relationTargetFieldMetadataIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
const { getInitialFilterValue } = useGetInitialFilterValue();
|
||||
|
||||
const applyAdvancedFilterRelationTargetField = ({
|
||||
sourceFieldMetadataItem,
|
||||
relationTargetFieldMetadataItem,
|
||||
recordFilterId,
|
||||
}: ApplyAdvancedFilterRelationTargetFieldParams) => {
|
||||
setFieldMetadataItemIdUsedInDropdown(sourceFieldMetadataItem.id);
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(
|
||||
relationTargetFieldMetadataItem.type,
|
||||
);
|
||||
|
||||
const firstOperand = getRecordFilterOperands({
|
||||
filterType,
|
||||
subFieldName: null,
|
||||
})?.[0];
|
||||
|
||||
if (!isDefined(firstOperand)) {
|
||||
throw new Error(
|
||||
`No valid operand found for relation-traversal filter type: ${filterType}`,
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedOperandInDropdown(firstOperand);
|
||||
|
||||
const { value, displayValue } = getInitialFilterValue(
|
||||
filterType,
|
||||
firstOperand,
|
||||
);
|
||||
|
||||
const existingRecordFilter = currentRecordFilters.find(
|
||||
(recordFilter) => recordFilter.id === recordFilterId,
|
||||
);
|
||||
|
||||
const newAdvancedFilter = {
|
||||
id: recordFilterId,
|
||||
fieldMetadataId: sourceFieldMetadataItem.id,
|
||||
displayValue,
|
||||
operand: firstOperand,
|
||||
value,
|
||||
recordFilterGroupId: existingRecordFilter?.recordFilterGroupId,
|
||||
positionInRecordFilterGroup:
|
||||
existingRecordFilter?.positionInRecordFilterGroup,
|
||||
type: filterType,
|
||||
label: `${sourceFieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}`,
|
||||
subFieldName: null,
|
||||
relationTargetFieldMetadataId: relationTargetFieldMetadataItem.id,
|
||||
} satisfies RecordFilter;
|
||||
|
||||
setSubFieldNameUsedInDropdown(null);
|
||||
setRelationTargetFieldMetadataIdUsedInDropdown(
|
||||
relationTargetFieldMetadataItem.id,
|
||||
);
|
||||
|
||||
setObjectFilterDropdownSearchInput('');
|
||||
|
||||
setObjectFilterDropdownCurrentRecordFilter(newAdvancedFilter);
|
||||
upsertRecordFilter(newAdvancedFilter);
|
||||
};
|
||||
|
||||
return {
|
||||
applyAdvancedFilterRelationTargetField,
|
||||
};
|
||||
};
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
|
||||
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 { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
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';
|
||||
|
||||
type ApplyAdvancedFilterSourceFieldParams = {
|
||||
sourceFieldMetadataItem: FieldMetadataItem;
|
||||
recordFilterId: string;
|
||||
};
|
||||
|
||||
// Creates a leaf advanced filter from a source field selection. Composite
|
||||
// and many-to-one relation sources are handled by their own specialized
|
||||
// hooks (useApplyAdvancedFilterCompositeSubField,
|
||||
// useApplyAdvancedFilterRelationTargetField) — callers branch to those
|
||||
// before reaching this hook.
|
||||
export const useApplyAdvancedFilterSourceField = () => {
|
||||
const setSelectedOperandInDropdown = useSetAtomComponentState(
|
||||
selectedOperandInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setFieldMetadataItemIdUsedInDropdown = useSetAtomComponentState(
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownSearchInput = useSetAtomComponentState(
|
||||
objectFilterDropdownSearchInputComponentState,
|
||||
);
|
||||
|
||||
const setSubFieldNameUsedInDropdown = useSetAtomComponentState(
|
||||
subFieldNameUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setRelationTargetFieldMetadataIdUsedInDropdown =
|
||||
useSetAtomComponentState(
|
||||
relationTargetFieldMetadataIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const setObjectFilterDropdownCurrentRecordFilter = useSetAtomComponentState(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { upsertRecordFilter } = useUpsertRecordFilter();
|
||||
const { getInitialFilterValue } = useGetInitialFilterValue();
|
||||
|
||||
const applyAdvancedFilterSourceField = ({
|
||||
sourceFieldMetadataItem,
|
||||
recordFilterId,
|
||||
}: ApplyAdvancedFilterSourceFieldParams) => {
|
||||
setFieldMetadataItemIdUsedInDropdown(sourceFieldMetadataItem.id);
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(sourceFieldMetadataItem.type);
|
||||
|
||||
const firstOperand = getRecordFilterOperands({
|
||||
filterType,
|
||||
subFieldName: null,
|
||||
})?.[0];
|
||||
|
||||
if (!isDefined(firstOperand)) {
|
||||
throw new Error(`No valid operand found for filter type: ${filterType}`);
|
||||
}
|
||||
|
||||
setSelectedOperandInDropdown(firstOperand);
|
||||
|
||||
const { value, displayValue } = getInitialFilterValue(
|
||||
filterType,
|
||||
firstOperand,
|
||||
);
|
||||
|
||||
const existingRecordFilter = currentRecordFilters.find(
|
||||
(recordFilter) => recordFilter.id === recordFilterId,
|
||||
);
|
||||
|
||||
const newAdvancedFilter = {
|
||||
id: recordFilterId,
|
||||
fieldMetadataId: sourceFieldMetadataItem.id,
|
||||
displayValue,
|
||||
operand: firstOperand,
|
||||
value,
|
||||
recordFilterGroupId: existingRecordFilter?.recordFilterGroupId,
|
||||
positionInRecordFilterGroup:
|
||||
existingRecordFilter?.positionInRecordFilterGroup,
|
||||
type: filterType,
|
||||
label: sourceFieldMetadataItem.label,
|
||||
subFieldName: null,
|
||||
relationTargetFieldMetadataId: null,
|
||||
} satisfies RecordFilter;
|
||||
|
||||
setSubFieldNameUsedInDropdown(null);
|
||||
setRelationTargetFieldMetadataIdUsedInDropdown(null);
|
||||
|
||||
setObjectFilterDropdownSearchInput('');
|
||||
|
||||
setObjectFilterDropdownCurrentRecordFilter(newAdvancedFilter);
|
||||
upsertRecordFilter(newAdvancedFilter);
|
||||
};
|
||||
|
||||
return {
|
||||
applyAdvancedFilterSourceField,
|
||||
};
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
|
||||
// RELATION and SELECT leaf fields open a value picker keyed by the field
|
||||
// id once the user finishes choosing the field — push it on the focus
|
||||
// stack so the picker's keyboard hotkeys are active when it opens. Other
|
||||
// field types use input-style pickers that don't rely on the focus stack.
|
||||
export const usePushFocusForLeafFieldValuePicker = () => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const pushFocusForLeafFieldValuePicker = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
if (
|
||||
fieldMetadataItem.type !== 'RELATION' &&
|
||||
fieldMetadataItem.type !== 'SELECT'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: fieldMetadataItem.id,
|
||||
component: {
|
||||
type: FocusComponentType.DROPDOWN,
|
||||
instanceId: fieldMetadataItem.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { pushFocusForLeafFieldValuePicker };
|
||||
};
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FILTER_FIELD_LIST_ID } from '@/object-record/object-filter-dropdown/constants/FilterFieldListId';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
@@ -27,9 +28,9 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
||||
|
||||
const Icon = getIcon(fieldMetadataItemToSelect.icon);
|
||||
|
||||
const shouldShowSubMenu = isCompositeFieldType(
|
||||
fieldMetadataItemToSelect.type,
|
||||
);
|
||||
const shouldShowSubMenu =
|
||||
isCompositeFieldType(fieldMetadataItemToSelect.type) ||
|
||||
isManyToOneRelationField(fieldMetadataItemToSelect);
|
||||
|
||||
const handleClick = () => {
|
||||
resetSelectedItem();
|
||||
|
||||
+33
-6
@@ -1,7 +1,10 @@
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
|
||||
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
|
||||
import { DATE_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/object-filter-dropdown/constants/DatePickerDropdownContentWidth';
|
||||
import { useApplyObjectFilterDropdownOperand } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand';
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||
@@ -13,6 +16,7 @@ import { DropdownMenuInnerSelect } from '@/ui/layout/dropdown/components/Dropdow
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
@@ -32,11 +36,34 @@ export const ObjectFilterDropdownInnerSelectOperandDropdown = () => {
|
||||
subFieldNameUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const operandsForFilterType = isDefined(fieldMetadataItemUsedInDropdown)
|
||||
const relationTargetFieldMetadataIdUsedInDropdown =
|
||||
useAtomComponentStateValue(
|
||||
relationTargetFieldMetadataIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
// The target field may have been deleted from the workspace since the
|
||||
// filter was saved — return null and let the parent skip rendering
|
||||
// rather than throwing.
|
||||
const relationTargetFieldMetadataItem = isDefined(
|
||||
relationTargetFieldMetadataIdUsedInDropdown,
|
||||
)
|
||||
? getFieldMetadataItemById({
|
||||
fieldMetadataId: relationTargetFieldMetadataIdUsedInDropdown,
|
||||
objectMetadataItems,
|
||||
}).fieldMetadataItem
|
||||
: null;
|
||||
|
||||
const effectiveFieldMetadataItem = isDefined(
|
||||
relationTargetFieldMetadataIdUsedInDropdown,
|
||||
)
|
||||
? relationTargetFieldMetadataItem
|
||||
: fieldMetadataItemUsedInDropdown;
|
||||
|
||||
const operandsForFilterType = isDefined(effectiveFieldMetadataItem)
|
||||
? getRecordFilterOperands({
|
||||
filterType: getFilterTypeFromFieldType(
|
||||
fieldMetadataItemUsedInDropdown.type,
|
||||
),
|
||||
filterType: getFilterTypeFromFieldType(effectiveFieldMetadataItem.type),
|
||||
subFieldName: subFieldNameUsedInDropdown,
|
||||
})
|
||||
: [];
|
||||
@@ -70,13 +97,13 @@ export const ObjectFilterDropdownInnerSelectOperandDropdown = () => {
|
||||
|
||||
if (
|
||||
!isDefined(selectedOperandInDropdown) ||
|
||||
!isDefined(fieldMetadataItemUsedInDropdown)
|
||||
!isDefined(effectiveFieldMetadataItem)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(
|
||||
fieldMetadataItemUsedInDropdown.type,
|
||||
effectiveFieldMetadataItem.type,
|
||||
);
|
||||
|
||||
const isDateFilter = DATE_FILTER_TYPES.includes(filterType);
|
||||
|
||||
+9
@@ -5,6 +5,7 @@ import { objectFilterDropdownAnyFieldSearchIsSelectedComponentState } from '@/ob
|
||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||
import { objectFilterDropdownIsSelectingRelationTargetFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingRelationTargetFieldComponentState';
|
||||
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
@@ -45,6 +46,12 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const objectFilterDropdownIsSelectingRelationTargetField =
|
||||
useAtomComponentStateCallbackState(
|
||||
objectFilterDropdownIsSelectingRelationTargetFieldComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const objectFilterDropdownCurrentRecordFilter =
|
||||
useAtomComponentStateCallbackState(
|
||||
objectFilterDropdownCurrentRecordFilterComponentState,
|
||||
@@ -56,6 +63,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
|
||||
store.set(selectedOperandInDropdown, null);
|
||||
store.set(objectFilterDropdownFilterIsSelected, false);
|
||||
store.set(objectFilterDropdownIsSelectingCompositeField, false);
|
||||
store.set(objectFilterDropdownIsSelectingRelationTargetField, false);
|
||||
store.set(fieldMetadataItemIdUsedInDropdown, null);
|
||||
store.set(objectFilterDropdownCurrentRecordFilter, null);
|
||||
store.set(objectFilterDropdownAnyFieldSearchIsSelected, false);
|
||||
@@ -64,6 +72,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
|
||||
selectedOperandInDropdown,
|
||||
objectFilterDropdownFilterIsSelected,
|
||||
objectFilterDropdownIsSelectingCompositeField,
|
||||
objectFilterDropdownIsSelectingRelationTargetField,
|
||||
fieldMetadataItemIdUsedInDropdown,
|
||||
objectFilterDropdownCurrentRecordFilter,
|
||||
objectFilterDropdownAnyFieldSearchIsSelected,
|
||||
|
||||
+9
@@ -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,
|
||||
});
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
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 objectFilterDropdownSubMenuFieldTypeComponentState =
|
||||
createAtomComponentState<CompositeFilterableFieldType | null>({
|
||||
createAtomComponentState<ObjectFilterDropdownSubMenuFieldType | null>({
|
||||
key: 'objectFilterDropdownSubMenuFieldTypeComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
|
||||
|
||||
+9
@@ -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 relationTargetFieldMetadataIdUsedInDropdownComponentState =
|
||||
createAtomComponentState<string | null>({
|
||||
key: 'relationTargetFieldMetadataIdUsedInDropdownComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
|
||||
});
|
||||
+19
-6
@@ -1,3 +1,4 @@
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
|
||||
import { useRecordCalendarMonthDaysRange } from '@/object-record/record-calendar/month/hooks/useRecordCalendarMonthDaysRange';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
@@ -6,8 +7,10 @@ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type Temporal } from 'temporal-polyfill';
|
||||
@@ -46,6 +49,10 @@ export const useRecordCalendarQueryDateRangeFilter = (
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const anyFieldFilterValue = useAtomComponentStateValue(
|
||||
anyFieldFilterValueComponentState,
|
||||
viewBarInstanceId,
|
||||
@@ -94,15 +101,21 @@ export const useRecordCalendarQueryDateRangeFilter = (
|
||||
displayValue: `${lastDayOfLastWeek.toString()}`,
|
||||
};
|
||||
|
||||
const calendarRecordFilters = [
|
||||
...currentRecordFilters,
|
||||
dateRangeFilterAfter,
|
||||
dateRangeFilterBefore,
|
||||
];
|
||||
|
||||
const dateRangeFilter = computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilters: [
|
||||
...currentRecordFilters,
|
||||
dateRangeFilterAfter,
|
||||
dateRangeFilterBefore,
|
||||
],
|
||||
recordFilters: calendarRecordFilters,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: calendarRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
});
|
||||
|
||||
const { recordGqlOperationFilter: anyFieldFilter } =
|
||||
|
||||
+2
-1
@@ -156,7 +156,8 @@ export const useGetRecordFilterDisplayValue = () => {
|
||||
}
|
||||
|
||||
const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow(
|
||||
recordFilter.fieldMetadataId,
|
||||
recordFilter.relationTargetFieldMetadataId ??
|
||||
recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
const fieldMetadataItemOptions = fieldMetadataItem.options;
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { type CompositeFilterableFieldType } from '@/object-record/record-filter/types/CompositeFilterableFieldType';
|
||||
|
||||
export type ObjectFilterDropdownSubMenuFieldType = CompositeFilterableFieldType;
|
||||
@@ -24,7 +24,7 @@ export type RecordFilter = {
|
||||
positionInRecordFilterGroup?: number | null;
|
||||
label: string;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
// RLS-specific: when set, filter compares against current user's field value
|
||||
relationTargetFieldMetadataId?: string | null;
|
||||
rlsDynamicValue?: RLSDynamicValue | null;
|
||||
};
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
// Relation-traversal filters reference target fields that live on a
|
||||
// different object than the one being queried, so they aren't in the
|
||||
// source object's own field list. The shared GraphQL filter dispatcher
|
||||
// looks the target up by id in `fieldMetadataItems` and drops the filter
|
||||
// when it can't find it — this helper merges the resolved target fields
|
||||
// in from the workspace-wide flat list so callers don't silently lose
|
||||
// relation-traversal filters.
|
||||
export const augmentFieldsWithRelationTargets = ({
|
||||
baseFields,
|
||||
recordFilters,
|
||||
allFieldMetadataItems,
|
||||
}: {
|
||||
baseFields: FieldMetadataItem[];
|
||||
recordFilters: Pick<RecordFilter, 'relationTargetFieldMetadataId'>[];
|
||||
allFieldMetadataItems: FieldMetadataItem[];
|
||||
}): FieldMetadataItem[] => {
|
||||
const targetFieldIds = new Set(
|
||||
recordFilters
|
||||
.map((filter) => filter.relationTargetFieldMetadataId)
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
if (targetFieldIds.size === 0) {
|
||||
return baseFields;
|
||||
}
|
||||
|
||||
const baseFieldIds = new Set(baseFields.map((field) => field.id));
|
||||
const additionalTargetFields = allFieldMetadataItems.filter(
|
||||
(field) => targetFieldIds.has(field.id) && !baseFieldIds.has(field.id),
|
||||
);
|
||||
|
||||
if (additionalTargetFields.length === 0) {
|
||||
return baseFields;
|
||||
}
|
||||
|
||||
return [...baseFields, ...additionalTargetFields];
|
||||
};
|
||||
+7
@@ -6,11 +6,13 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -52,11 +54,16 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const computedFilter = computeContextStoreFilters({
|
||||
contextStoreTargetedRecordsRule,
|
||||
contextStoreFilters,
|
||||
contextStoreFilterGroups,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
contextStoreAnyFieldFilterValue,
|
||||
});
|
||||
|
||||
+7
@@ -7,6 +7,7 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
|
||||
@@ -18,6 +19,7 @@ import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -87,6 +89,10 @@ export const useRecordIndexLazyFetchRecords = ({
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const findManyRecordsParams = useFindManyRecordIndexTableParams(
|
||||
objectMetadataItem.nameSingular,
|
||||
recordIndexId,
|
||||
@@ -101,6 +107,7 @@ export const useRecordIndexLazyFetchRecords = ({
|
||||
contextStoreFilters,
|
||||
contextStoreFilterGroups,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
contextStoreAnyFieldFilterValue,
|
||||
});
|
||||
|
||||
+12
-1
@@ -1,14 +1,17 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
|
||||
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import {
|
||||
combineFilters,
|
||||
computeRecordGqlOperationFilter,
|
||||
@@ -47,8 +50,16 @@ export const useFindManyRecordIndexTableParams = (
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const currentFilters = computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem?.fields ?? [],
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem?.fields ?? [],
|
||||
recordFilters: currentRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
recordFilters: currentRecordFilters,
|
||||
filterValueDependencies,
|
||||
|
||||
+6
-4
@@ -2,6 +2,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
||||
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
|
||||
@@ -30,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';
|
||||
@@ -128,10 +128,12 @@ export const useLoadRecordIndexStates = () => {
|
||||
.map(mapViewFieldToRecordField)
|
||||
.filter(isDefined);
|
||||
|
||||
const allFilterableFields = getFilterableFields(objectMetadataItem);
|
||||
const flattenedFieldMetadataItems = store.get(
|
||||
flattenedFieldMetadataItemsSelector.atom,
|
||||
);
|
||||
const recordFilters = mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
allFilterableFields,
|
||||
flattenedFieldMetadataItems,
|
||||
);
|
||||
|
||||
const recordFilterGroups = mapViewFilterGroupsToRecordFilterGroups(
|
||||
@@ -140,7 +142,7 @@ export const useLoadRecordIndexStates = () => {
|
||||
|
||||
const contextStoreFilters = mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterableFieldMetadataItems,
|
||||
flattenedFieldMetadataItems,
|
||||
);
|
||||
|
||||
let recordIndexGroupFieldMetadataItemValue = undefined;
|
||||
|
||||
+12
-1
@@ -1,10 +1,12 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useRelevantRecordsGqlFields } from '@/object-record/record-field/hooks/useRelevantRecordsGqlFields';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector';
|
||||
import { computeRecordGroupOptionsFilter } from '@/object-record/record-group/utils/computeRecordGroupOptionsFilter';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
@@ -12,6 +14,7 @@ import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import {
|
||||
combineFilters,
|
||||
computeRecordGqlOperationFilter,
|
||||
@@ -36,11 +39,19 @@ export const useRecordIndexGroupCommonQueryVariables = () => {
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const requestFilters = computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilters: currentRecordFilters,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: currentRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
});
|
||||
|
||||
const anyFieldFilterValue = useAtomComponentStateValue(
|
||||
|
||||
+12
-1
@@ -1,4 +1,5 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
@@ -8,10 +9,12 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record-
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { useAggregateGqlFieldsFromRecordIndexGroupAggregates } from '@/object-record/record-index/hooks/useAggregateGqlFieldsFromRecordIndexGroupAggregates';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { useMemo } from 'react';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
@@ -46,11 +49,19 @@ export const useRecordIndexGroupsAggregatesGroupBy = ({
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const requestFilters = computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilters: currentRecordFilters,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: currentRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
});
|
||||
|
||||
const { recordAggregateGqlField } =
|
||||
|
||||
+13
-1
@@ -1,9 +1,11 @@
|
||||
import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent';
|
||||
import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { recordIndexHasRecordsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexHasRecordsComponentSelector';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
@@ -13,6 +15,7 @@ import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEvent
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -38,6 +41,10 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
@@ -57,7 +64,11 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
variables: {
|
||||
filter: computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: currentRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
recordFilters: currentRecordFilters,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
filterValueDependencies,
|
||||
@@ -71,6 +82,7 @@ export const RecordTableEmptyHasNewRecordEffect = () => {
|
||||
currentRecordFilterGroups,
|
||||
filterValueDependencies,
|
||||
currentRecordSorts,
|
||||
flattenedFieldMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+11
-1
@@ -1,3 +1,4 @@
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
|
||||
import { transformAggregateRawValueIntoAggregateDisplayValue } from '@/object-record/record-aggregate/utils/transformAggregateRawValueIntoAggregateDisplayValue';
|
||||
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
|
||||
@@ -6,6 +7,7 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record-
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter';
|
||||
import { getRecordAggregateDisplayLabel } from '@/object-record/record-index/utils/getRecordndexAggregateDisplayLabel';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
@@ -45,10 +47,18 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
|
||||
|
||||
const dateLocale = useAtomStateValue(dateLocaleState);
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const requestFilters = computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: currentRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
filterValueDependencies,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
recordFilters: currentRecordFilters,
|
||||
|
||||
+13
-1
@@ -1,19 +1,26 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableVirtualizedSSESubscribeEffect = () => {
|
||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
@@ -33,7 +40,11 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
variables: {
|
||||
filter: computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: currentRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
recordFilters: currentRecordFilters,
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
filterValueDependencies,
|
||||
@@ -47,6 +58,7 @@ export const RecordTableVirtualizedSSESubscribeEffect = () => {
|
||||
currentRecordFilterGroups,
|
||||
filterValueDependencies,
|
||||
currentRecordSorts,
|
||||
flattenedFieldMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+7
@@ -4,9 +4,11 @@ import { contextStoreFilterGroupsComponentState } from '@/context-store/states/c
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type UseUpdateMultipleRecordsActionsProps = {
|
||||
objectNameSingular: string;
|
||||
@@ -43,11 +45,16 @@ export const useUpdateMultipleRecordsActions = ({
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const graphqlFilter = computeContextStoreFilters({
|
||||
contextStoreTargetedRecordsRule,
|
||||
contextStoreFilters,
|
||||
contextStoreFilterGroups,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
contextStoreAnyFieldFilterValue,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||
import {
|
||||
@@ -26,10 +27,7 @@ export const prefillRecord = <T extends ObjectRecord>({
|
||||
const fieldValue = isUndefined(inputValue)
|
||||
? generateEmptyFieldValue({ fieldMetadataItem })
|
||||
: inputValue;
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE
|
||||
) {
|
||||
if (isManyToOneRelationField(fieldMetadataItem)) {
|
||||
const joinColumnName = computeRelationGqlFieldJoinColumnName({
|
||||
name: fieldMetadataItem.name,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField';
|
||||
@@ -63,8 +64,7 @@ export const sanitizeRecordInput = ({
|
||||
|
||||
if (
|
||||
isDefined(fieldMetadataItem) &&
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.relation?.type === RelationType.MANY_TO_ONE &&
|
||||
isManyToOneRelationField(fieldMetadataItem) &&
|
||||
!isDefined(recordInput[fieldMetadataItem.name]?.connect?.where)
|
||||
) {
|
||||
return undefined;
|
||||
|
||||
+15
-2
@@ -1,5 +1,8 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import {
|
||||
computeRecordGqlOperationFilter,
|
||||
isDefined,
|
||||
@@ -38,10 +41,20 @@ export const useGraphWidgetQueryCommon = ({
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const widgetRecordFilters = configuration.filter?.recordFilters ?? [];
|
||||
|
||||
const gqlOperationFilter = computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: widgetRecordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
filterValueDependencies,
|
||||
recordFilters: configuration.filter?.recordFilters ?? [],
|
||||
recordFilters: widgetRecordFilters,
|
||||
recordFilterGroups: configuration.filter?.recordFilterGroups ?? [],
|
||||
});
|
||||
|
||||
|
||||
+17
-9
@@ -7,7 +7,8 @@ import { CoreObjectNameSingular, FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { AdvancedFilterFieldSelectSearchInput } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectSearchInput';
|
||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
||||
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
|
||||
import { useApplyAdvancedFilterSourceField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterSourceField';
|
||||
import { usePushFocusForLeafFieldValuePicker } from '@/object-record/advanced-filter/hooks/usePushFocusForLeafFieldValuePicker';
|
||||
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
|
||||
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
@@ -73,8 +74,8 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF
|
||||
advancedFilterFieldSelectDropdownId,
|
||||
);
|
||||
|
||||
const { selectFieldUsedInAdvancedFilterDropdown } =
|
||||
useSelectFieldUsedInAdvancedFilterDropdown();
|
||||
const { applyAdvancedFilterSourceField } =
|
||||
useApplyAdvancedFilterSourceField();
|
||||
|
||||
const [, setObjectFilterDropdownSubMenuFieldType] = useAtomComponentState(
|
||||
objectFilterDropdownSubMenuFieldTypeComponentState,
|
||||
@@ -89,6 +90,9 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF
|
||||
fieldMetadataItemIdUsedInDropdownComponentState,
|
||||
);
|
||||
|
||||
const { pushFocusForLeafFieldValuePicker } =
|
||||
usePushFocusForLeafFieldValuePicker();
|
||||
|
||||
const handleFieldSelect = (
|
||||
selectedFieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
@@ -102,13 +106,17 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectF
|
||||
setObjectFilterDropdownSubMenuFieldType(filterType);
|
||||
setFieldMetadataItemIdUsedInDropdown(selectedFieldMetadataItem.id);
|
||||
setObjectFilterDropdownIsSelectingCompositeField(true);
|
||||
} else {
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: selectedFieldMetadataItem.id,
|
||||
recordFilterId,
|
||||
});
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
applyAdvancedFilterSourceField({
|
||||
sourceFieldMetadataItem: selectedFieldMetadataItem,
|
||||
recordFilterId,
|
||||
});
|
||||
|
||||
pushFocusForLeafFieldValuePicker(selectedFieldMetadataItem);
|
||||
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
};
|
||||
|
||||
const selectableItemIdArray = filteredFieldMetadataItems.map(
|
||||
|
||||
+6
-6
@@ -10,7 +10,7 @@ import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
|
||||
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
|
||||
import { useApplyAdvancedFilterCompositeSubField } from '@/object-record/advanced-filter/hooks/useApplyAdvancedFilterCompositeSubField';
|
||||
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';
|
||||
@@ -61,8 +61,8 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectS
|
||||
const { closeAdvancedFilterFieldSelectDropdown } =
|
||||
useAdvancedFilterFieldSelectDropdown(recordFilterId);
|
||||
|
||||
const { selectFieldUsedInAdvancedFilterDropdown } =
|
||||
useSelectFieldUsedInAdvancedFilterDropdown();
|
||||
const { applyAdvancedFilterCompositeSubField } =
|
||||
useApplyAdvancedFilterCompositeSubField();
|
||||
|
||||
const handleSelectFilter = (
|
||||
selectedFieldMetadataItem: FieldMetadataItem | null | undefined,
|
||||
@@ -72,10 +72,10 @@ export const SettingsRolePermissionsObjectLevelRecordLevelPermissionFieldSelectS
|
||||
return;
|
||||
}
|
||||
|
||||
selectFieldUsedInAdvancedFilterDropdown({
|
||||
fieldMetadataItemId: selectedFieldMetadataItem.id,
|
||||
applyAdvancedFilterCompositeSubField({
|
||||
sourceFieldMetadataItem: selectedFieldMetadataItem,
|
||||
subFieldName: subFieldName ?? null,
|
||||
recordFilterId,
|
||||
subFieldName,
|
||||
});
|
||||
|
||||
closeAdvancedFilterFieldSelectDropdown();
|
||||
|
||||
+1
-4
@@ -71,10 +71,7 @@ export const RecordTableSettingsFilters = ({
|
||||
isWorkflowFindRecords={false}
|
||||
onUpdate={handleFilterUpdate}
|
||||
/>
|
||||
<RecordTableSettingsFiltersInitializeStateEffect
|
||||
view={view}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
<RecordTableSettingsFiltersInitializeStateEffect view={view} />
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
</RecordFilterGroupsComponentInstanceContext.Provider>
|
||||
</StyledFilterSettingsContainer>
|
||||
|
||||
+8
-7
@@ -1,11 +1,11 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
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';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
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';
|
||||
@@ -13,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,
|
||||
@@ -39,6 +37,10 @@ export const RecordTableSettingsFiltersInitializeStateEffect = ({
|
||||
currentRecordFilterGroupsComponentState,
|
||||
);
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const [hasInitializedFilters, setHasInitializedFilters] = useState(false);
|
||||
|
||||
const stateAlreadyHasFilters =
|
||||
@@ -54,10 +56,9 @@ export const RecordTableSettingsFiltersInitializeStateEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const filterableFields = getFilterableFields(objectMetadataItem);
|
||||
const recordFilters = mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterableFields,
|
||||
flattenedFieldMetadataItems,
|
||||
);
|
||||
|
||||
setCurrentRecordFilters(recordFilters);
|
||||
@@ -71,7 +72,7 @@ export const RecordTableSettingsFiltersInitializeStateEffect = ({
|
||||
setHasInitializedFilters(true);
|
||||
}, [
|
||||
view,
|
||||
objectMetadataItem,
|
||||
flattenedFieldMetadataItems,
|
||||
hasInitializedFilters,
|
||||
stateAlreadyHasFilters,
|
||||
setCurrentRecordFilters,
|
||||
|
||||
+2
-4
@@ -1,12 +1,10 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { isManyToOneRelationField } from '@/object-metadata/utils/isManyToOneRelationField';
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const hasNestedFields = (fieldMetadata: FieldMetadataItem) => {
|
||||
return (
|
||||
(fieldMetadata.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE) ||
|
||||
isManyToOneRelationField(fieldMetadata) ||
|
||||
isCompositeFieldType(fieldMetadata.type)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export const VIEW_FILTER_FRAGMENT = gql`
|
||||
viewFilterGroupId
|
||||
positionInViewFilterGroup
|
||||
subFieldName
|
||||
relationTargetFieldMetadataId
|
||||
viewId
|
||||
createdAt
|
||||
updatedAt
|
||||
|
||||
+1
@@ -125,6 +125,7 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
|
||||
label: mockFieldMetadataItem.label,
|
||||
type: getFilterTypeFromFieldType(mockFieldMetadataItem.type),
|
||||
subFieldName: null,
|
||||
relationTargetFieldMetadataId: null,
|
||||
} satisfies RecordFilter,
|
||||
]);
|
||||
});
|
||||
|
||||
+12
-1
@@ -1,11 +1,14 @@
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useGetViewGroupsFilters } from '@/views/hooks/useGetViewGroupsFilters';
|
||||
import {
|
||||
computeRecordGqlOperationFilter,
|
||||
@@ -25,13 +28,21 @@ export const useGetRecordIndexTotalCount = () => {
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const recordGroupsVisibilityFilter = useGetViewGroupsFilters();
|
||||
|
||||
const filter = computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter],
|
||||
recordFilterGroups: currentRecordFilterGroups,
|
||||
fields: objectMetadataItem.fields,
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem.fields,
|
||||
recordFilters: [...currentRecordFilters, ...recordGroupsVisibilityFilter],
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
});
|
||||
|
||||
const anyFieldFilterValue = useAtomComponentStateValue(
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
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,
|
||||
);
|
||||
|
||||
const mapViewFiltersToRecordFilters = (
|
||||
viewFilters: ViewFilter[] | GqlViewFilter[],
|
||||
) => {
|
||||
const filterableFieldMetadataItems =
|
||||
getFilterableFields(objectMetadataItem);
|
||||
return mapViewFiltersToFilters(viewFilters, filterableFieldMetadataItems);
|
||||
return mapViewFiltersToFilters(viewFilters, flattenedFieldMetadataItems);
|
||||
};
|
||||
|
||||
return { mapViewFiltersToRecordFilters };
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { getQueryVariablesFromFiltersAndSorts } from '@/views/utils/getQueryVariablesFromFiltersAndSorts';
|
||||
|
||||
export const useQueryVariablesFromParentView = ({
|
||||
@@ -13,6 +15,10 @@ export const useQueryVariablesFromParentView = ({
|
||||
}) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const flattenedFieldMetadataItems = useAtomStateValue(
|
||||
flattenedFieldMetadataItemsSelector,
|
||||
);
|
||||
|
||||
const contextStoreRecordShowParentView = useAtomComponentStateValue(
|
||||
contextStoreRecordShowParentViewComponentState,
|
||||
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
@@ -27,6 +33,7 @@ export const useQueryVariablesFromParentView = ({
|
||||
recordSorts: contextStoreRecordShowParentView?.parentViewSorts ?? [],
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
flattenedFieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
});
|
||||
|
||||
|
||||
+4
@@ -144,6 +144,8 @@ export const useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters =
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -158,6 +160,8 @@ export const useSaveRecordFiltersAndGroupFiltersToViewFiltersAndGroupFilters =
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
+10
@@ -1,5 +1,6 @@
|
||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||
import { relationTargetFieldMetadataIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/relationTargetFieldMetadataIdUsedInDropdownComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
|
||||
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
|
||||
@@ -59,6 +60,15 @@ export const useSetEditableFilterChipDropdownStates = () => {
|
||||
}),
|
||||
recordFilter.subFieldName,
|
||||
);
|
||||
|
||||
store.set(
|
||||
relationTargetFieldMetadataIdUsedInDropdownComponentState.atomFamily({
|
||||
instanceId: getEditableChipObjectFilterDropdownComponentInstanceId({
|
||||
recordFilterId: recordFilter.id,
|
||||
}),
|
||||
}),
|
||||
recordFilter.relationTargetFieldMetadataId ?? null,
|
||||
);
|
||||
},
|
||||
[store, filterableFieldMetadataItems],
|
||||
);
|
||||
|
||||
@@ -12,4 +12,5 @@ export type ViewFilter = {
|
||||
viewFilterGroupId?: string | null;
|
||||
positionInViewFilterGroup?: number | null;
|
||||
subFieldName?: string | null;
|
||||
relationTargetFieldMetadataId?: string | null;
|
||||
};
|
||||
|
||||
@@ -43,6 +43,8 @@ describe('mapViewFiltersToFilters', () => {
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
positionInRecordFilterGroup: undefined,
|
||||
recordFilterGroupId: undefined,
|
||||
subFieldName: undefined,
|
||||
relationTargetFieldMetadataId: null,
|
||||
},
|
||||
];
|
||||
expect(
|
||||
|
||||
@@ -12,6 +12,7 @@ export const areViewFiltersEqual = (
|
||||
'value',
|
||||
'operand',
|
||||
'subFieldName',
|
||||
'relationTargetFieldMetadataId',
|
||||
];
|
||||
|
||||
return propertiesToCompare.every((property) =>
|
||||
|
||||
+9
-1
@@ -1,7 +1,9 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { augmentFieldsWithRelationTargets } from '@/object-record/record-filter/utils/augmentFieldsWithRelationTargets';
|
||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { type RecordFilterValueDependencies } from 'twenty-shared/types';
|
||||
import { computeRecordGqlOperationFilter } from 'twenty-shared/utils';
|
||||
@@ -12,6 +14,7 @@ export const getQueryVariablesFromFiltersAndSorts = ({
|
||||
recordSorts,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems = [],
|
||||
flattenedFieldMetadataItems,
|
||||
filterValueDependencies,
|
||||
}: {
|
||||
recordFilterGroups: RecordFilterGroup[];
|
||||
@@ -19,10 +22,15 @@ export const getQueryVariablesFromFiltersAndSorts = ({
|
||||
recordSorts: RecordSort[];
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
objectMetadataItems?: EnrichedObjectMetadataItem[];
|
||||
flattenedFieldMetadataItems: FieldMetadataItem[];
|
||||
filterValueDependencies: RecordFilterValueDependencies;
|
||||
}) => {
|
||||
const filter = computeRecordGqlOperationFilter({
|
||||
fields: objectMetadataItem?.fields ?? [],
|
||||
fields: augmentFieldsWithRelationTargets({
|
||||
baseFields: objectMetadataItem?.fields ?? [],
|
||||
recordFilters,
|
||||
allFieldMetadataItems: flattenedFieldMetadataItems,
|
||||
}),
|
||||
filterValueDependencies,
|
||||
recordFilterGroups,
|
||||
recordFilters,
|
||||
|
||||
@@ -13,5 +13,7 @@ export const mapRecordFilterToViewFilter = (
|
||||
positionInViewFilterGroup: recordFilter.positionInRecordFilterGroup,
|
||||
viewFilterGroupId: recordFilter.recordFilterGroupId,
|
||||
subFieldName: recordFilter.subFieldName,
|
||||
relationTargetFieldMetadataId:
|
||||
recordFilter.relationTargetFieldMetadataId ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,27 +14,50 @@ import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
|
||||
export const mapViewFiltersToFilters = (
|
||||
viewFilters: ViewFilter[] | GqlViewFilter[],
|
||||
availableFieldMetadataItems: FieldMetadataItem[],
|
||||
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 filterType = getFilterTypeFromFieldType(
|
||||
availableFieldMetadataItem.type,
|
||||
);
|
||||
const relationTargetFieldMetadataItem = isDefined(
|
||||
viewFilter.relationTargetFieldMetadataId,
|
||||
)
|
||||
? fieldMetadataItems.find(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === viewFilter.relationTargetFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const label = isSystemSearchVectorField(availableFieldMetadataItem.name)
|
||||
// A relation-traversal filter is meaningful only if both ends
|
||||
// resolve — drop it otherwise (same race-condition handling as the
|
||||
// source field above) instead of rendering a chip whose filterType
|
||||
// and label fall back to the relation source, since the dispatcher
|
||||
// would silently drop the GraphQL filter anyway.
|
||||
if (
|
||||
isDefined(viewFilter.relationTargetFieldMetadataId) &&
|
||||
!isDefined(relationTargetFieldMetadataItem)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const filterType = isDefined(relationTargetFieldMetadataItem)
|
||||
? getFilterTypeFromFieldType(relationTargetFieldMetadataItem.type)
|
||||
: getFilterTypeFromFieldType(sourceFieldMetadataItem.type);
|
||||
|
||||
const label = isSystemSearchVectorField(sourceFieldMetadataItem.name)
|
||||
? 'Search'
|
||||
: availableFieldMetadataItem.label;
|
||||
: isDefined(relationTargetFieldMetadataItem)
|
||||
? `${sourceFieldMetadataItem.label} → ${relationTargetFieldMetadataItem.label}`
|
||||
: sourceFieldMetadataItem.label;
|
||||
|
||||
const operand = viewFilter.operand;
|
||||
|
||||
@@ -54,6 +77,8 @@ export const mapViewFiltersToFilters = (
|
||||
label,
|
||||
type: filterType,
|
||||
subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
} satisfies RecordFilter;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.6.0', 1798000005000)
|
||||
export class AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."viewFilter" ADD "relationTargetFieldMetadataId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_VIEW_FILTER_RELATION_TARGET_FIELD_METADATA_ID" ON "core"."viewFilter" ("relationTargetFieldMetadataId") WHERE "relationTargetFieldMetadataId" IS NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."viewFilter" ADD CONSTRAINT "FK_dbe259395cbd9a54c1c17d12b0b" FOREIGN KEY ("relationTargetFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."viewFilter" DROP CONSTRAINT "FK_dbe259395cbd9a54c1c17d12b0b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_VIEW_FILTER_RELATION_TARGET_FIELD_METADATA_ID"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."viewFilter" DROP COLUMN "relationTargetFieldMetadataId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -42,6 +42,7 @@ import { EncryptSensitiveConfigStorageSlowInstanceCommand } from 'src/database/c
|
||||
import { EncryptTotpSecretsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-slow-1798000009000-encrypt-totp-secrets';
|
||||
import { AddSubFieldNameToViewSortFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1778502963794-add-sub-field-name-to-view-sort';
|
||||
import { DropPostgresCredentialsTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1798500000000-drop-postgres-credentials-table';
|
||||
import { AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-6/2-6-instance-command-fast-1798000005000-add-relation-target-field-metadata-id-to-view-filter';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
@@ -86,4 +87,5 @@ export const INSTANCE_COMMANDS = [
|
||||
EncryptTotpSecretsSlowInstanceCommand,
|
||||
AddSubFieldNameToViewSortFastInstanceCommand,
|
||||
DropPostgresCredentialsTableFastInstanceCommand,
|
||||
AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand,
|
||||
];
|
||||
|
||||
+29
@@ -231,6 +231,8 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
recordFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -254,6 +256,33 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
options: field.options as PartialFieldMetadataItemOption[],
|
||||
}));
|
||||
|
||||
// Relation-traversal filters reference target fields on related objects
|
||||
// that aren't in the source object's field list above. Resolve and add
|
||||
// them so the shared dispatcher can look them up by id.
|
||||
const relationTargetFieldsFromFilters = recordFilters
|
||||
.map((filter) => filter.relationTargetFieldMetadataId)
|
||||
.filter(isDefined)
|
||||
.filter((id) => !fields.some((field) => field.id === id))
|
||||
.map((id) => {
|
||||
const field = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!field) return null;
|
||||
|
||||
return {
|
||||
id: field.id,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
label: field.label,
|
||||
options: field.options as PartialFieldMetadataItemOption[],
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
fields.push(...relationTargetFieldsFromFilters);
|
||||
|
||||
const filtersFromView = computeRecordGqlOperationFilter({
|
||||
recordFilters,
|
||||
recordFilterGroups: recordFilterGroups,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export const fromViewFilterManifestToUniversalFlatViewFilter = ({
|
||||
operand: viewFilterManifest.operand,
|
||||
value: viewFilterManifest.value,
|
||||
subFieldName: viewFilterManifest.subFieldName ?? null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
positionInViewFilterGroup:
|
||||
viewFilterManifest.positionInViewFilterGroup ?? null,
|
||||
createdAt: now,
|
||||
|
||||
+1
@@ -356,6 +356,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"viewFilterGroupUniversalIdentifier",
|
||||
"positionInViewFilterGroup",
|
||||
"subFieldName",
|
||||
"relationTargetFieldMetadataUniversalIdentifier",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"value",
|
||||
|
||||
+5
@@ -669,6 +669,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
relationTargetFieldMetadataId: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: 'relationTargetFieldMetadataUniversalIdentifier',
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
|
||||
+3
@@ -132,6 +132,9 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
view: {
|
||||
foreignKey: 'viewId',
|
||||
},
|
||||
relationTargetFieldMetadata: {
|
||||
foreignKey: 'relationTargetFieldMetadataId',
|
||||
},
|
||||
viewFilterGroup: {
|
||||
foreignKey: 'viewFilterGroupId',
|
||||
},
|
||||
|
||||
+7
@@ -241,6 +241,13 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
|
||||
isNullable: false,
|
||||
universalForeignKey: 'viewUniversalIdentifier',
|
||||
},
|
||||
relationTargetFieldMetadata: {
|
||||
metadataName: 'fieldMetadata',
|
||||
foreignKey: 'relationTargetFieldMetadataId',
|
||||
inverseOneToManyProperty: null,
|
||||
isNullable: true,
|
||||
universalForeignKey: 'relationTargetFieldMetadataUniversalIdentifier',
|
||||
},
|
||||
viewFilterGroup: {
|
||||
metadataName: 'viewFilterGroup',
|
||||
foreignKey: 'viewFilterGroupId',
|
||||
|
||||
+1
-1
@@ -4,10 +4,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
|
||||
[
|
||||
"rowLevelPermissionPredicate",
|
||||
"navigationMenuItem",
|
||||
"viewFilter",
|
||||
"commandMenuItem",
|
||||
"fieldPermission",
|
||||
"viewField",
|
||||
"viewFilter",
|
||||
"objectPermission",
|
||||
"pageLayoutWidget",
|
||||
"viewSort",
|
||||
|
||||
+1
@@ -7,4 +7,5 @@ export const FLAT_VIEW_FILTER_EDITABLE_PROPERTIES = [
|
||||
'viewFilterGroupId',
|
||||
'positionInViewFilterGroup',
|
||||
'subFieldName',
|
||||
'relationTargetFieldMetadataId',
|
||||
] as const satisfies MetadataEntityPropertyName<'viewFilter'>[];
|
||||
|
||||
+5
@@ -31,6 +31,7 @@ export const fromCreateViewFilterInputToFlatViewFilterToCreate = ({
|
||||
'viewFilterGroupId',
|
||||
'operand',
|
||||
'subFieldName',
|
||||
'relationTargetFieldMetadataId',
|
||||
],
|
||||
);
|
||||
|
||||
@@ -41,12 +42,15 @@ export const fromCreateViewFilterInputToFlatViewFilterToCreate = ({
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilter',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId,
|
||||
viewId,
|
||||
viewFilterGroupId: createViewFilterInput.viewFilterGroupId,
|
||||
relationTargetFieldMetadataId:
|
||||
createViewFilterInput.relationTargetFieldMetadataId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
@@ -69,6 +73,7 @@ export const fromCreateViewFilterInputToFlatViewFilterToCreate = ({
|
||||
positionInViewFilterGroup:
|
||||
createViewFilterInput.positionInViewFilterGroup ?? null,
|
||||
subFieldName: createViewFilterInput.subFieldName ?? null,
|
||||
relationTargetFieldMetadataUniversalIdentifier,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
};
|
||||
};
|
||||
|
||||
+17
@@ -87,5 +87,22 @@ export const fromUpdateViewFilterInputToFlatViewFilterToUpdateOrThrow = ({
|
||||
viewFilterGroupUniversalIdentifier;
|
||||
}
|
||||
|
||||
if (
|
||||
updatedEditableFieldProperties.relationTargetFieldMetadataId !== undefined
|
||||
) {
|
||||
const { relationTargetFieldMetadataUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilter',
|
||||
foreignKeyValues: {
|
||||
relationTargetFieldMetadataId:
|
||||
flatViewFilterToUpdate.relationTargetFieldMetadataId,
|
||||
},
|
||||
flatEntityMaps: { flatFieldMetadataMaps },
|
||||
});
|
||||
|
||||
flatViewFilterToUpdate.relationTargetFieldMetadataUniversalIdentifier =
|
||||
relationTargetFieldMetadataUniversalIdentifier;
|
||||
}
|
||||
|
||||
return flatViewFilterToUpdate;
|
||||
};
|
||||
|
||||
+17
@@ -69,6 +69,22 @@ export const fromViewFilterEntityToFlatViewFilter = ({
|
||||
);
|
||||
}
|
||||
|
||||
let relationTargetFieldMetadataUniversalIdentifier: string | null = null;
|
||||
|
||||
if (isDefined(viewFilterEntity.relationTargetFieldMetadataId)) {
|
||||
relationTargetFieldMetadataUniversalIdentifier =
|
||||
fieldMetadataIdToUniversalIdentifierMap.get(
|
||||
viewFilterEntity.relationTargetFieldMetadataId,
|
||||
) ?? null;
|
||||
|
||||
if (!isDefined(relationTargetFieldMetadataUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`FieldMetadata with id ${viewFilterEntity.relationTargetFieldMetadataId} not found for viewFilter ${viewFilterEntity.id} (relation target)`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...viewFilterEntityWithoutRelations,
|
||||
createdAt: viewFilterEntity.createdAt.toISOString(),
|
||||
@@ -77,6 +93,7 @@ export const fromViewFilterEntityToFlatViewFilter = ({
|
||||
universalIdentifier: viewFilterEntityWithoutRelations.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
};
|
||||
|
||||
+5
@@ -49,6 +49,11 @@ export class CreateViewFilterInput {
|
||||
@Field({ nullable: true })
|
||||
subFieldName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
relationTargetFieldMetadataId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
viewId: string;
|
||||
|
||||
+5
@@ -46,6 +46,11 @@ class UpdateViewFilterInputUpdates {
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
subFieldName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
relationTargetFieldMetadataId?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
@@ -34,6 +34,9 @@ export class ViewFilterDTO {
|
||||
@Field(() => String, { nullable: true })
|
||||
subFieldName?: string | null;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
relationTargetFieldMetadataId?: string | null;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: false })
|
||||
viewId: string;
|
||||
|
||||
|
||||
+15
@@ -23,6 +23,11 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
@Index('IDX_VIEW_FILTER_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId'])
|
||||
@Index('IDX_VIEW_FILTER_VIEW_ID', ['viewId'])
|
||||
@Index('IDX_VIEW_FILTER_FIELD_METADATA_ID', ['fieldMetadataId'])
|
||||
@Index(
|
||||
'IDX_VIEW_FILTER_RELATION_TARGET_FIELD_METADATA_ID',
|
||||
['relationTargetFieldMetadataId'],
|
||||
{ where: '"relationTargetFieldMetadataId" IS NOT NULL' },
|
||||
)
|
||||
export class ViewFilterEntity
|
||||
extends SyncableEntity
|
||||
implements Required<ViewFilterEntity>
|
||||
@@ -59,6 +64,16 @@ export class ViewFilterEntity
|
||||
@Column({ nullable: true, type: 'text', default: null })
|
||||
subFieldName: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid', default: null })
|
||||
relationTargetFieldMetadataId: string | null;
|
||||
|
||||
@ManyToOne(() => FieldMetadataEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'relationTargetFieldMetadataId' })
|
||||
relationTargetFieldMetadata: Relation<FieldMetadataEntity> | null;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
viewId: string;
|
||||
|
||||
|
||||
+5
@@ -48,4 +48,9 @@ export class UpsertViewWidgetViewFilterInput {
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
subFieldName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
relationTargetFieldMetadataId?: string;
|
||||
}
|
||||
|
||||
+78
@@ -267,5 +267,83 @@ describe('ViewQueryParamsService', () => {
|
||||
// Filter should be effectively empty because the field was deleted
|
||||
expect(result.filter).toEqual({ and: [] });
|
||||
});
|
||||
|
||||
it('should resolve relation-traversal filters against the target field', async () => {
|
||||
const relationFieldId = 'relation-field-id';
|
||||
const targetFieldId = 'target-field-id';
|
||||
const mockFilterGroupId = 'filter-group-id';
|
||||
|
||||
const flatFieldMetadataMapsWithRelation = {
|
||||
byUniversalIdentifier: {
|
||||
'relation-universal-id': {
|
||||
id: relationFieldId,
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
options: null,
|
||||
universalIdentifier: 'relation-universal-id',
|
||||
},
|
||||
'target-universal-id': {
|
||||
id: targetFieldId,
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
options: null,
|
||||
universalIdentifier: 'target-universal-id',
|
||||
},
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[relationFieldId]: 'relation-universal-id',
|
||||
[targetFieldId]: 'target-universal-id',
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
};
|
||||
|
||||
const mockView = {
|
||||
id: mockViewId,
|
||||
name: 'People at Acme',
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
type: ViewType.TABLE,
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
viewFilters: [
|
||||
{
|
||||
id: 'filter-id',
|
||||
fieldMetadataId: relationFieldId,
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
value: 'Acme',
|
||||
viewFilterGroupId: mockFilterGroupId,
|
||||
subFieldName: null,
|
||||
relationTargetFieldMetadataId: targetFieldId,
|
||||
},
|
||||
],
|
||||
viewFilterGroups: [
|
||||
{
|
||||
id: mockFilterGroupId,
|
||||
parentViewFilterGroupId: null,
|
||||
logicalOperator: ViewFilterGroupLogicalOperator.AND,
|
||||
},
|
||||
],
|
||||
viewSorts: [],
|
||||
};
|
||||
|
||||
viewService.findByIdWithRelations.mockResolvedValue(mockView as any);
|
||||
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatObjectMetadataMaps: mockFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: flatFieldMetadataMapsWithRelation,
|
||||
} as any,
|
||||
);
|
||||
|
||||
const result = await viewQueryParamsService.resolveViewToQueryParams(
|
||||
mockViewId,
|
||||
mockWorkspaceId,
|
||||
);
|
||||
|
||||
// Filter is nested under the relation field name, not flattened
|
||||
// against the FK column.
|
||||
expect(result.filter).toEqual({
|
||||
and: [{ company: { name: { ilike: '%Acme%' } } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+11
-3
@@ -90,6 +90,8 @@ export class ViewQueryParamsService {
|
||||
recordFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
operand: viewFilter.operand,
|
||||
subFieldName: viewFilter.subFieldName,
|
||||
relationTargetFieldMetadataId:
|
||||
viewFilter.relationTargetFieldMetadataId ?? null,
|
||||
} as RecordFilter;
|
||||
})
|
||||
.filter(isDefined);
|
||||
@@ -105,10 +107,16 @@ export class ViewQueryParamsService {
|
||||
: RecordFilterGroupLogicalOperator.AND,
|
||||
}));
|
||||
|
||||
const fields = recordFilters
|
||||
.map((filter) => {
|
||||
const filterFieldMetadataIds = recordFilters.flatMap((filter) =>
|
||||
isDefined(filter.relationTargetFieldMetadataId)
|
||||
? [filter.fieldMetadataId, filter.relationTargetFieldMetadataId]
|
||||
: [filter.fieldMetadataId],
|
||||
);
|
||||
|
||||
const fields = filterFieldMetadataIds
|
||||
.map((fieldMetadataId) => {
|
||||
const field = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: filter.fieldMetadataId,
|
||||
flatEntityId: fieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
|
||||
+15
-1
@@ -675,12 +675,15 @@ export class ViewWidgetUpsertService {
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilter',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputFilter.fieldMetadataId,
|
||||
viewId,
|
||||
viewFilterGroupId: inputFilter.viewFilterGroupId,
|
||||
relationTargetFieldMetadataId:
|
||||
inputFilter.relationTargetFieldMetadataId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
@@ -706,6 +709,9 @@ export class ViewWidgetUpsertService {
|
||||
positionInViewFilterGroup:
|
||||
inputFilter.positionInViewFilterGroup ?? null,
|
||||
subFieldName: inputFilter.subFieldName ?? null,
|
||||
relationTargetFieldMetadataId:
|
||||
inputFilter.relationTargetFieldMetadataId ?? null,
|
||||
relationTargetFieldMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
@@ -719,17 +725,22 @@ export class ViewWidgetUpsertService {
|
||||
existingFilter.viewFilterGroupId !== inputFilter.viewFilterGroupId ||
|
||||
existingFilter.positionInViewFilterGroup !==
|
||||
inputFilter.positionInViewFilterGroup ||
|
||||
existingFilter.subFieldName !== inputFilter.subFieldName;
|
||||
existingFilter.subFieldName !== inputFilter.subFieldName ||
|
||||
existingFilter.relationTargetFieldMetadataId !==
|
||||
(inputFilter.relationTargetFieldMetadataId ?? null);
|
||||
|
||||
if (hasChanged) {
|
||||
const {
|
||||
fieldMetadataUniversalIdentifier,
|
||||
viewFilterGroupUniversalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewFilter',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: inputFilter.fieldMetadataId,
|
||||
viewFilterGroupId: inputFilter.viewFilterGroupId,
|
||||
relationTargetFieldMetadataId:
|
||||
inputFilter.relationTargetFieldMetadataId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
@@ -751,6 +762,9 @@ export class ViewWidgetUpsertService {
|
||||
existingFilter.positionInViewFilterGroup,
|
||||
subFieldName:
|
||||
inputFilter.subFieldName ?? existingFilter.subFieldName,
|
||||
relationTargetFieldMetadataId:
|
||||
inputFilter.relationTargetFieldMetadataId ?? null,
|
||||
relationTargetFieldMetadataUniversalIdentifier,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
+2
@@ -96,6 +96,8 @@ export const createStandardViewFilterFlatMetadata = <
|
||||
operand,
|
||||
value,
|
||||
subFieldName,
|
||||
relationTargetFieldMetadataId: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
positionInViewFilterGroup,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
||||
+11
-6
@@ -26,12 +26,16 @@ export class CreateViewFilterActionHandlerService extends WorkspaceMigrationRunn
|
||||
flatApplication,
|
||||
workspaceId,
|
||||
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateViewFilterAction>): Promise<FlatCreateViewFilterAction> {
|
||||
const { fieldMetadataId, viewId, viewFilterGroupId } =
|
||||
resolveUniversalRelationIdentifiersToIds({
|
||||
flatEntityMaps: allFlatEntityMaps,
|
||||
metadataName: action.metadataName,
|
||||
universalForeignKeyValues: action.flatEntity,
|
||||
});
|
||||
const {
|
||||
fieldMetadataId,
|
||||
viewId,
|
||||
viewFilterGroupId,
|
||||
relationTargetFieldMetadataId,
|
||||
} = resolveUniversalRelationIdentifiersToIds({
|
||||
flatEntityMaps: allFlatEntityMaps,
|
||||
metadataName: action.metadataName,
|
||||
universalForeignKeyValues: action.flatEntity,
|
||||
});
|
||||
|
||||
const emptyUniversalForeignKeyAggregators =
|
||||
getUniversalFlatEntityEmptyForeignKeyAggregators({
|
||||
@@ -45,6 +49,7 @@ export class CreateViewFilterActionHandlerService extends WorkspaceMigrationRunn
|
||||
fieldMetadataId,
|
||||
viewId,
|
||||
viewFilterGroupId,
|
||||
relationTargetFieldMetadataId,
|
||||
id: action.id ?? v4(),
|
||||
applicationId: flatApplication.id,
|
||||
workspaceId,
|
||||
|
||||
+15
-1
@@ -69,7 +69,21 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const fields = flatObjectMetadata.fieldIds
|
||||
// Relation-traversal filters reference target fields on related
|
||||
// objects, which aren't in flatObjectMetadata.fieldIds. Collect the
|
||||
// referenced target ids from the record filters so the shared
|
||||
// dispatcher can resolve them and avoid silently dropping the filter.
|
||||
const relationTargetFieldIds = (
|
||||
workflowActionInput.filter?.recordFilters ?? []
|
||||
)
|
||||
.map((filter) => filter.relationTargetFieldMetadataId)
|
||||
.filter(isDefined);
|
||||
|
||||
const fieldIds = Array.from(
|
||||
new Set([...flatObjectMetadata.fieldIds, ...relationTargetFieldIds]),
|
||||
);
|
||||
|
||||
const fields = fieldIds
|
||||
.map((fieldId) => {
|
||||
const field = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: fieldId,
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { createOneViewFilter } from 'test/integration/metadata/suites/view-filter/utils/create-one-view-filter.util';
|
||||
import { destroyOneViewFilter } from 'test/integration/metadata/suites/view-filter/utils/destroy-one-view-filter.util';
|
||||
import { updateOneViewFilter } from 'test/integration/metadata/suites/view-filter/utils/update-one-view-filter.util';
|
||||
import { createOneView } from 'test/integration/metadata/suites/view/utils/create-one-view.util';
|
||||
import { destroyOneView } from 'test/integration/metadata/suites/view/utils/destroy-one-view.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import { ViewFilterOperand, ViewType } from 'twenty-shared/types';
|
||||
|
||||
// Round-trip of relation-traversal view filters (the one-hop
|
||||
// `relationTargetFieldMetadataId` carried alongside the source relation
|
||||
// field id) through the metadata create + update mutations.
|
||||
const RELATION_TRAVERSAL_GQL_FIELDS = `
|
||||
id
|
||||
fieldMetadataId
|
||||
operand
|
||||
value
|
||||
viewId
|
||||
subFieldName
|
||||
relationTargetFieldMetadataId
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
`;
|
||||
|
||||
describe('View Filter relation-traversal round-trip should succeed', () => {
|
||||
let createdViewId: string;
|
||||
let createdViewFilterId: string;
|
||||
let personCompanyRelationFieldId: string;
|
||||
let companyNameFieldId: string;
|
||||
let companyDomainNameFieldId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: { first: 1000 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
type
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(objects);
|
||||
|
||||
const personObject = objects.find(
|
||||
(object: { nameSingular: string }) => object.nameSingular === 'person',
|
||||
);
|
||||
jestExpectToBeDefined(personObject);
|
||||
|
||||
const companyObject = objects.find(
|
||||
(object: { nameSingular: string }) => object.nameSingular === 'company',
|
||||
);
|
||||
jestExpectToBeDefined(companyObject);
|
||||
|
||||
const personCompanyRelationField = personObject.fieldsList?.find(
|
||||
(field: { name: string; type: string }) =>
|
||||
field.name === 'company' && field.type === 'RELATION',
|
||||
);
|
||||
jestExpectToBeDefined(personCompanyRelationField);
|
||||
personCompanyRelationFieldId = personCompanyRelationField.id;
|
||||
|
||||
const companyNameField = companyObject.fieldsList?.find(
|
||||
(field: { name: string }) => field.name === 'name',
|
||||
);
|
||||
jestExpectToBeDefined(companyNameField);
|
||||
companyNameFieldId = companyNameField.id;
|
||||
|
||||
const companyDomainNameField = companyObject.fieldsList?.find(
|
||||
(field: { name: string }) => field.name === 'domainName',
|
||||
);
|
||||
jestExpectToBeDefined(companyDomainNameField);
|
||||
companyDomainNameFieldId = companyDomainNameField.id;
|
||||
|
||||
const { data: viewData } = await createOneView({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'Test View For Relation Traversal Filter',
|
||||
objectMetadataId: personObject.id,
|
||||
type: ViewType.TABLE,
|
||||
icon: 'IconUser',
|
||||
},
|
||||
});
|
||||
|
||||
createdViewId = viewData?.createView?.id;
|
||||
jestExpectToBeDefined(createdViewId);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdViewId) {
|
||||
await destroyOneView({
|
||||
expectToFail: false,
|
||||
viewId: createdViewId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdViewFilterId) {
|
||||
await destroyOneViewFilter({
|
||||
expectToFail: false,
|
||||
input: { id: createdViewFilterId },
|
||||
});
|
||||
createdViewFilterId = '';
|
||||
}
|
||||
});
|
||||
|
||||
it('should persist relationTargetFieldMetadataId on create', async () => {
|
||||
const { data } = await createOneViewFilter({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
viewId: createdViewId,
|
||||
fieldMetadataId: personCompanyRelationFieldId,
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
value: 'Acme',
|
||||
relationTargetFieldMetadataId: companyNameFieldId,
|
||||
},
|
||||
gqlFields: RELATION_TRAVERSAL_GQL_FIELDS,
|
||||
});
|
||||
|
||||
createdViewFilterId = data?.createViewFilter?.id;
|
||||
|
||||
expect(data.createViewFilter).toMatchObject({
|
||||
id: expect.any(String),
|
||||
viewId: createdViewId,
|
||||
fieldMetadataId: personCompanyRelationFieldId,
|
||||
relationTargetFieldMetadataId: companyNameFieldId,
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
value: 'Acme',
|
||||
});
|
||||
});
|
||||
|
||||
it('should default relationTargetFieldMetadataId to null when not provided', async () => {
|
||||
const { data } = await createOneViewFilter({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
viewId: createdViewId,
|
||||
fieldMetadataId: personCompanyRelationFieldId,
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
value: 'Acme',
|
||||
},
|
||||
gqlFields: RELATION_TRAVERSAL_GQL_FIELDS,
|
||||
});
|
||||
|
||||
createdViewFilterId = data?.createViewFilter?.id;
|
||||
|
||||
expect(data.createViewFilter).toMatchObject({
|
||||
id: expect.any(String),
|
||||
viewId: createdViewId,
|
||||
fieldMetadataId: personCompanyRelationFieldId,
|
||||
relationTargetFieldMetadataId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update relationTargetFieldMetadataId on an existing filter', async () => {
|
||||
const { data: createData } = await createOneViewFilter({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
viewId: createdViewId,
|
||||
fieldMetadataId: personCompanyRelationFieldId,
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
value: 'Acme',
|
||||
relationTargetFieldMetadataId: companyNameFieldId,
|
||||
},
|
||||
gqlFields: RELATION_TRAVERSAL_GQL_FIELDS,
|
||||
});
|
||||
|
||||
createdViewFilterId = createData?.createViewFilter?.id;
|
||||
jestExpectToBeDefined(createdViewFilterId);
|
||||
|
||||
const { data: updateData } = await updateOneViewFilter({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: createdViewFilterId,
|
||||
update: {
|
||||
relationTargetFieldMetadataId: companyDomainNameFieldId,
|
||||
},
|
||||
},
|
||||
gqlFields: RELATION_TRAVERSAL_GQL_FIELDS,
|
||||
});
|
||||
|
||||
expect(updateData.updateViewFilter).toMatchObject({
|
||||
id: createdViewFilterId,
|
||||
fieldMetadataId: personCompanyRelationFieldId,
|
||||
relationTargetFieldMetadataId: companyDomainNameFieldId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve relationTargetFieldMetadataId when an unrelated property is updated', async () => {
|
||||
const { data: createData } = await createOneViewFilter({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
viewId: createdViewId,
|
||||
fieldMetadataId: personCompanyRelationFieldId,
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
value: 'Acme',
|
||||
relationTargetFieldMetadataId: companyNameFieldId,
|
||||
},
|
||||
gqlFields: RELATION_TRAVERSAL_GQL_FIELDS,
|
||||
});
|
||||
|
||||
createdViewFilterId = createData?.createViewFilter?.id;
|
||||
jestExpectToBeDefined(createdViewFilterId);
|
||||
|
||||
const { data: updateData } = await updateOneViewFilter({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: createdViewFilterId,
|
||||
update: {
|
||||
value: 'Acme Corp',
|
||||
},
|
||||
},
|
||||
gqlFields: RELATION_TRAVERSAL_GQL_FIELDS,
|
||||
});
|
||||
|
||||
expect(updateData.updateViewFilter).toMatchObject({
|
||||
id: createdViewFilterId,
|
||||
value: 'Acme Corp',
|
||||
relationTargetFieldMetadataId: companyNameFieldId,
|
||||
});
|
||||
});
|
||||
});
|
||||
+88
@@ -919,4 +919,92 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
expect(result).toHaveProperty('recordId.in');
|
||||
});
|
||||
});
|
||||
|
||||
describe('relation traversal', () => {
|
||||
// The dispatcher should wrap the inner filter under the relation source
|
||||
// field's GraphQL key and build it against the target field's type
|
||||
// (not the relation FK's type).
|
||||
it('should nest filter under source field name when target field is set', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: {
|
||||
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
|
||||
relationTargetFieldMetadataId: 'f-text',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ company: { name: { ilike: '%Acme%' } } });
|
||||
});
|
||||
|
||||
// If the target field is no longer in fieldMetadataItems (e.g. it was
|
||||
// deleted from the workspace), dropping the filter is the safe path —
|
||||
// the alternative would silently interpret the text value as a UUID
|
||||
// list against the relation FK.
|
||||
it('should return undefined when target field is not found', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: {
|
||||
...makeFilter('f-relation', RecordFilterOperand.CONTAINS, 'Acme'),
|
||||
relationTargetFieldMetadataId: 'nonexistent-target',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
// Emptiness operands must check the target column on the related
|
||||
// object rather than the FK column on the source record.
|
||||
it('should apply IS_EMPTY against the target field, not the relation FK', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: {
|
||||
...makeFilter('f-relation', RecordFilterOperand.IS_EMPTY, ''),
|
||||
relationTargetFieldMetadataId: 'f-text',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
// Without traversal, the RELATION case would have produced a filter
|
||||
// on `companyId`; here we expect the emptiness check to fall on
|
||||
// `company.name` instead.
|
||||
expect(result).toEqual({
|
||||
company: { or: [{ name: { ilike: '' } }, { name: { is: 'NULL' } }] },
|
||||
});
|
||||
});
|
||||
|
||||
// The target field's per-type switch must run, so number/select/etc.
|
||||
// targets behave like a direct filter on that field — verified here
|
||||
// with a SELECT target.
|
||||
it('should dispatch to target type switch (SELECT target)', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: {
|
||||
...makeFilter('f-relation', RecordFilterOperand.IS, '["ACTIVE"]'),
|
||||
relationTargetFieldMetadataId: 'f-select',
|
||||
} as RecordFilter,
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ company: { status: { in: ['ACTIVE'] } } });
|
||||
});
|
||||
|
||||
// Without a relationTargetFieldMetadataId the dispatcher must fall
|
||||
// through to the direct builder — preserving the filter-by-record-id
|
||||
// behaviour on the relation FK.
|
||||
it('should keep relation filter-by-id behaviour when no target field is set', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-relation',
|
||||
RecordFilterOperand.IS,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('companyId.in');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ export type RecordFilter = {
|
||||
recordFilterGroupId?: string | null;
|
||||
operand: ViewFilterOperand;
|
||||
subFieldName?: CompositeFieldSubFieldName | null | undefined;
|
||||
relationTargetFieldMetadataId?: string | null | undefined;
|
||||
};
|
||||
|
||||
export type RecordFilterGroup = {
|
||||
|
||||
+164
-127
@@ -75,11 +75,11 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
}: TurnRecordFilterIntoRecordGqlOperationFilterParams):
|
||||
| RecordGqlOperationFilter
|
||||
| undefined => {
|
||||
const correspondingFieldMetadataItem = fieldMetadataItems.find(
|
||||
const sourceFieldMetadataItem = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(correspondingFieldMetadataItem)) {
|
||||
if (!isDefined(sourceFieldMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,15 +87,66 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
sourceFieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
isDefined(recordFilter.relationTargetFieldMetadataId)
|
||||
) {
|
||||
const targetFieldMetadataItem = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.relationTargetFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetFieldMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const innerFilter = buildDirectFieldGqlOperationFilter({
|
||||
recordFilter: {
|
||||
...recordFilter,
|
||||
fieldMetadataId: targetFieldMetadataItem.id,
|
||||
relationTargetFieldMetadataId: null,
|
||||
},
|
||||
fieldMetadataItem: targetFieldMetadataItem,
|
||||
filterValueDependencies,
|
||||
});
|
||||
|
||||
if (!isDefined(innerFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
[sourceFieldMetadataItem.name]: innerFilter,
|
||||
} as RecordGqlOperationFilter;
|
||||
}
|
||||
|
||||
return buildDirectFieldGqlOperationFilter({
|
||||
recordFilter,
|
||||
fieldMetadataItem: sourceFieldMetadataItem,
|
||||
filterValueDependencies,
|
||||
});
|
||||
};
|
||||
|
||||
type BuildDirectFieldGqlOperationFilterParams = {
|
||||
filterValueDependencies: RecordFilterValueDependencies;
|
||||
recordFilter: Omit<RecordFilter, 'id'>;
|
||||
fieldMetadataItem: FieldShared;
|
||||
};
|
||||
|
||||
const buildDirectFieldGqlOperationFilter = ({
|
||||
recordFilter,
|
||||
fieldMetadataItem,
|
||||
filterValueDependencies,
|
||||
}: BuildDirectFieldGqlOperationFilterParams):
|
||||
| RecordGqlOperationFilter
|
||||
| undefined => {
|
||||
const shouldComputeEmptinessFilter = checkIfShouldComputeEmptinessFilter({
|
||||
recordFilterOperand: recordFilter.operand,
|
||||
correspondingFieldMetadataItem,
|
||||
correspondingFieldMetadataItem: fieldMetadataItem,
|
||||
});
|
||||
|
||||
if (shouldComputeEmptinessFilter) {
|
||||
const emptinessFilter = getEmptyRecordGqlOperationFilter({
|
||||
operand: recordFilter.operand,
|
||||
correspondingField: correspondingFieldMetadataItem,
|
||||
correspondingField: fieldMetadataItem,
|
||||
recordFilter: recordFilter,
|
||||
});
|
||||
|
||||
@@ -106,23 +157,21 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
const isSubFieldFilter = isNonEmptyString(subFieldName);
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(
|
||||
correspondingFieldMetadataItem.type,
|
||||
);
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
switch (filterType) {
|
||||
case 'TEXT':
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.CONTAINS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
} as StringFilter,
|
||||
};
|
||||
case RecordFilterOperand.DOES_NOT_CONTAIN:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
} as StringFilter,
|
||||
},
|
||||
@@ -137,7 +186,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.VECTOR_SEARCH:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
search: recordFilter.value,
|
||||
} as TSVectorFilter,
|
||||
};
|
||||
@@ -150,14 +199,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.CONTAINS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
like: `%${recordFilter.value}%`,
|
||||
} as RawJsonFilter,
|
||||
};
|
||||
case RecordFilterOperand.DOES_NOT_CONTAIN:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
like: `%${recordFilter.value}%`,
|
||||
} as RawJsonFilter,
|
||||
},
|
||||
@@ -171,14 +220,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.CONTAINS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
like: `%${recordFilter.value}%`,
|
||||
} as FilesFilter,
|
||||
};
|
||||
case RecordFilterOperand.DOES_NOT_CONTAIN:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
like: `%${recordFilter.value}%`,
|
||||
} as FilesFilter,
|
||||
},
|
||||
@@ -215,12 +264,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: start,
|
||||
} as DateFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: end,
|
||||
} as DateFilter,
|
||||
},
|
||||
@@ -241,19 +290,19 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS_IN_PAST:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: nowAsPlainDate,
|
||||
} as DateFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS_IN_FUTURE:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: nowAsPlainDate,
|
||||
} as DateFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS_TODAY: {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
eq: nowAsPlainDate,
|
||||
} as DateFilter,
|
||||
};
|
||||
@@ -265,14 +314,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS_AFTER: {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: plainDateFilter,
|
||||
} as DateFilter,
|
||||
};
|
||||
}
|
||||
case RecordFilterOperand.IS_BEFORE: {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: plainDateFilter,
|
||||
} as DateFilter,
|
||||
};
|
||||
@@ -280,7 +329,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
case RecordFilterOperand.IS: {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
eq: plainDateFilter,
|
||||
} as DateFilter,
|
||||
};
|
||||
@@ -331,12 +380,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: start.toInstant().toString(),
|
||||
} as DateTimeFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: end.toInstant().toString(),
|
||||
} as DateTimeFilter,
|
||||
},
|
||||
@@ -357,13 +406,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS_IN_PAST:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: now.toInstant().round('minute').toString(),
|
||||
} as DateTimeFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS_IN_FUTURE:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gt: now.toInstant().round('minute').toString(),
|
||||
} as DateTimeFilter,
|
||||
};
|
||||
@@ -371,12 +420,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: getPeriodStart(now, 'DAY').toInstant().toString(),
|
||||
} as DateTimeFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: getNextPeriodStart(now, 'DAY').toInstant().toString(),
|
||||
} as DateTimeFilter,
|
||||
},
|
||||
@@ -413,12 +462,12 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: start.toString(),
|
||||
} as DateTimeFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: end.toString(),
|
||||
} as DateTimeFilter,
|
||||
},
|
||||
@@ -431,14 +480,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS_AFTER: {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: resolvedDateTime.toString(),
|
||||
} as DateTimeFilter,
|
||||
};
|
||||
}
|
||||
case RecordFilterOperand.IS_BEFORE: {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lt: resolvedDateTime.toString(),
|
||||
} as DateTimeFilter,
|
||||
};
|
||||
@@ -454,13 +503,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
eq: convertRatingToRatingValue(parseFloat(recordFilter.value)),
|
||||
} as RatingFilter,
|
||||
};
|
||||
case RecordFilterOperand.GREATER_THAN_OR_EQUAL:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
in: convertGreaterThanOrEqualRatingToArrayOfRatingValues(
|
||||
parseFloat(recordFilter.value),
|
||||
),
|
||||
@@ -468,7 +517,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
};
|
||||
case RecordFilterOperand.LESS_THAN_OR_EQUAL:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
in: convertLessThanOrEqualRatingToArrayOfRatingValues(
|
||||
parseFloat(recordFilter.value),
|
||||
),
|
||||
@@ -483,26 +532,26 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.GREATER_THAN_OR_EQUAL:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
gte: parseFloat(recordFilter.value),
|
||||
} as FloatFilter,
|
||||
};
|
||||
case RecordFilterOperand.LESS_THAN_OR_EQUAL:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
lte: parseFloat(recordFilter.value),
|
||||
} as FloatFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
eq: parseFloat(recordFilter.value),
|
||||
} as FloatFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS_NOT:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
eq: parseFloat(recordFilter.value),
|
||||
} as FloatFilter,
|
||||
},
|
||||
@@ -535,7 +584,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name + 'Id']: {
|
||||
[fieldMetadataItem.name + 'Id']: {
|
||||
in: recordIds,
|
||||
} as RelationFilter,
|
||||
};
|
||||
@@ -545,13 +594,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name + 'Id']: {
|
||||
[fieldMetadataItem.name + 'Id']: {
|
||||
in: recordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name + 'Id']: {
|
||||
[fieldMetadataItem.name + 'Id']: {
|
||||
is: 'NULL',
|
||||
} as RelationFilter,
|
||||
},
|
||||
@@ -579,7 +628,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
if (parsedCurrencyCodes.length === 0) return undefined;
|
||||
|
||||
const gqlFilter: RecordGqlOperationFilter = {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
currencyCode: { in: parsedCurrencyCodes },
|
||||
} as CurrencyFilter,
|
||||
};
|
||||
@@ -607,26 +656,26 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.GREATER_THAN_OR_EQUAL:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
amountMicros: { gte: parseFloat(recordFilter.value) * 1000000 },
|
||||
} as CurrencyFilter,
|
||||
};
|
||||
case RecordFilterOperand.LESS_THAN_OR_EQUAL:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
amountMicros: { lte: parseFloat(recordFilter.value) * 1000000 },
|
||||
} as CurrencyFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
amountMicros: { eq: parseFloat(recordFilter.value) * 1000000 },
|
||||
} as CurrencyFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS_NOT:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
amountMicros: {
|
||||
eq: parseFloat(recordFilter.value) * 1000000,
|
||||
},
|
||||
@@ -646,7 +695,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
}
|
||||
case 'LINKS': {
|
||||
return computeGqlOperationFilterForLinks({
|
||||
correspondingFieldMetadataItem,
|
||||
correspondingFieldMetadataItem: fieldMetadataItem,
|
||||
recordFilter,
|
||||
subFieldName,
|
||||
});
|
||||
@@ -654,7 +703,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
case 'FULL_NAME': {
|
||||
const fullNameFilters = generateILikeFiltersForCompositeFields(
|
||||
recordFilter.value,
|
||||
correspondingFieldMetadataItem.name,
|
||||
fieldMetadataItem.name,
|
||||
['firstName', 'lastName'],
|
||||
);
|
||||
switch (recordFilter.operand) {
|
||||
@@ -665,7 +714,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
[subFieldName]: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -684,7 +733,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
} else {
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
[subFieldName]: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -705,42 +754,42 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressState: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCountry: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
} as AddressFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressPostcode: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -759,7 +808,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
}
|
||||
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
[subFieldName]: {
|
||||
in: parsedCountryCodes,
|
||||
} as AddressFilter,
|
||||
@@ -768,7 +817,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
}
|
||||
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
[subFieldName]: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
} as AddressFilter,
|
||||
@@ -783,7 +832,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressStreet1: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -791,7 +840,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressStreet1: {
|
||||
is: 'NULL',
|
||||
},
|
||||
@@ -803,7 +852,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressStreet2: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -811,7 +860,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressStreet2: {
|
||||
is: 'NULL',
|
||||
},
|
||||
@@ -823,7 +872,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCity: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -831,7 +880,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCity: {
|
||||
is: 'NULL',
|
||||
},
|
||||
@@ -843,7 +892,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressState: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -851,7 +900,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressState: {
|
||||
is: 'NULL',
|
||||
},
|
||||
@@ -863,7 +912,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressPostcode: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -871,7 +920,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressPostcode: {
|
||||
is: 'NULL',
|
||||
},
|
||||
@@ -883,7 +932,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCountry: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -891,7 +940,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCountry: {
|
||||
is: 'NULL',
|
||||
},
|
||||
@@ -918,7 +967,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCountry: {
|
||||
in: JSON.parse(recordFilter.value),
|
||||
} as AddressFilter,
|
||||
@@ -926,7 +975,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
addressCountry: {
|
||||
is: 'NULL',
|
||||
} as AddressFilter,
|
||||
@@ -940,7 +989,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
[subFieldName]: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
} as AddressFilter,
|
||||
@@ -948,7 +997,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
[subFieldName]: {
|
||||
is: 'NULL',
|
||||
} as AddressFilter,
|
||||
@@ -976,7 +1025,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
if (nonEmptyOptions.length > 0) {
|
||||
conditions.push({
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
containsAny: nonEmptyOptions,
|
||||
} as MultiSelectFilter,
|
||||
});
|
||||
@@ -984,7 +1033,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
if (emptyOptions.length > 0) {
|
||||
conditions.push({
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
isEmptyArray: true,
|
||||
} as MultiSelectFilter,
|
||||
});
|
||||
@@ -997,18 +1046,18 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
containsAny: nonEmptyOptions,
|
||||
} as MultiSelectFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
isEmptyArray: true,
|
||||
} as MultiSelectFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
is: 'NULL',
|
||||
} as MultiSelectFilter,
|
||||
},
|
||||
@@ -1034,7 +1083,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
if (nonEmptyOptions.length > 0) {
|
||||
conditions.push({
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
in: nonEmptyOptions,
|
||||
} as SelectFilter,
|
||||
});
|
||||
@@ -1042,7 +1091,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
if (emptyOptions.length > 0) {
|
||||
conditions.push({
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
is: 'NULL',
|
||||
} as SelectFilter,
|
||||
});
|
||||
@@ -1056,7 +1105,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
if (nonEmptyOptions.length > 0) {
|
||||
conditions.push({
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
in: nonEmptyOptions,
|
||||
} as SelectFilter,
|
||||
},
|
||||
@@ -1066,7 +1115,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
if (emptyOptions.length > 0) {
|
||||
conditions.push({
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
is: 'NULL',
|
||||
} as SelectFilter,
|
||||
},
|
||||
@@ -1085,14 +1134,14 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.CONTAINS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
containsIlike: `%${recordFilter.value}%`,
|
||||
} as ArrayFilter,
|
||||
};
|
||||
case RecordFilterOperand.DOES_NOT_CONTAIN:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
containsIlike: `%${recordFilter.value}%`,
|
||||
} as ArrayFilter,
|
||||
},
|
||||
@@ -1114,7 +1163,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
const parsedSources = JSON.parse(recordFilter.value) as string[];
|
||||
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
source: {
|
||||
in: parsedSources,
|
||||
} satisfies RelationFilter,
|
||||
@@ -1132,7 +1181,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
source: {
|
||||
in: parsedSources,
|
||||
} satisfies RelationFilter,
|
||||
@@ -1141,12 +1190,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
};
|
||||
}
|
||||
default: {
|
||||
const fieldForRecordFilter = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`,
|
||||
`Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1177,7 +1222,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
workspaceMemberId: {
|
||||
in: workspaceMemberIds,
|
||||
} satisfies UUIDFilter,
|
||||
@@ -1188,7 +1233,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
workspaceMemberId: {
|
||||
in: workspaceMemberIds,
|
||||
} satisfies UUIDFilter,
|
||||
@@ -1196,7 +1241,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
workspaceMemberId: {
|
||||
is: 'NULL',
|
||||
} satisfies UUIDFilter,
|
||||
@@ -1206,12 +1251,8 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
};
|
||||
}
|
||||
default: {
|
||||
const fieldForRecordFilter = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`,
|
||||
`Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1227,7 +1268,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
name: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -1236,7 +1277,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
...(matchingSourceValues.length > 0
|
||||
? [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
source: {
|
||||
in: matchingSourceValues,
|
||||
},
|
||||
@@ -1252,7 +1293,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
name: {
|
||||
ilike: `%${recordFilter.value}%`,
|
||||
},
|
||||
@@ -1263,7 +1304,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
? [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
source: {
|
||||
in: matchingSourceValues,
|
||||
},
|
||||
@@ -1276,19 +1317,15 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
};
|
||||
}
|
||||
default: {
|
||||
const fieldForRecordFilter = fieldMetadataItems.find(
|
||||
(field) => field.id === recordFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Unknown operand ${recordFilter.operand} for ${fieldForRecordFilter?.label ?? ''} filter`,
|
||||
`Unknown operand ${recordFilter.operand} for ${fieldMetadataItem.label} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'EMAILS': {
|
||||
return computeGqlOperationFilterForEmails({
|
||||
correspondingFieldMetadataItem,
|
||||
correspondingFieldMetadataItem: fieldMetadataItem,
|
||||
recordFilter,
|
||||
subFieldName,
|
||||
});
|
||||
@@ -1306,21 +1343,21 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1333,7 +1370,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1342,7 +1379,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1353,7 +1390,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1361,7 +1398,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
additionalPhones: {
|
||||
is: 'NULL',
|
||||
} as PhonesFilter,
|
||||
@@ -1387,7 +1424,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1400,7 +1437,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1408,7 +1445,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
additionalPhones: {
|
||||
is: 'NULL',
|
||||
} as PhonesFilter,
|
||||
@@ -1426,7 +1463,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.CONTAINS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1435,7 +1472,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
case RecordFilterOperand.DOES_NOT_CONTAIN:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1452,7 +1489,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.CONTAINS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1461,7 +1498,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
case RecordFilterOperand.DOES_NOT_CONTAIN:
|
||||
return {
|
||||
not: {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
@@ -1482,7 +1519,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
}
|
||||
case 'BOOLEAN': {
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
eq: recordFilter.value === 'true',
|
||||
} as BooleanFilter,
|
||||
};
|
||||
@@ -1495,7 +1532,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS:
|
||||
return {
|
||||
[correspondingFieldMetadataItem.name]: {
|
||||
[fieldMetadataItem.name]: {
|
||||
in: recordIds,
|
||||
} as UUIDFilter,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user