fix(page-layout): render relation field widgets in table display mode (#22220)

Adding a to-many relation field as a **Table** on a record page rendered
an empty widget (header only) in several cases. This fixes three
independent defects behind that.

- **Morph inverse relations crashed the table.** The host-scoping view
filter (`IS current record`) is built on the relation's inverse field.
When that inverse is a `MORPH_RELATION` (attachments, notes, tasks…),
`getFilterTypeFromFieldType` fell through to `TEXT` and the GraphQL
builder threw `Unknown operand IS for TEXT filter`, unmounting the table
via the ErrorBoundary. `MORPH_RELATION` now classifies as `RELATION`,
and the relation filter resolves the correct morph join column (e.g.
`targetPersonId`) from the current record's object type.
- **Stale `viewId` on field change.** Changing the bound field on a
Table widget kept the previous relation's draft view (wrong
object/fields/filter). Field selection now regenerates the draft view
for the new relation, or clears the stale `viewId` when the new field
can't back a table.
- **Label identifier could be hidden or reordered.** Relation-table
widget views now pin the label-identifier field first and visible on
view creation and save.

Deferred: morph relation filters with arbitrary selected record ids (not
just "current record") — needs target-object identity in the filter
value schema.

**Test:** open a Person → edit layout → add a Field widget → bind a
to-many relation → switch Layout to Table. Previously empty for
`attachments` (morph) and for any field changed on an existing Table
widget; now scoped to the host record.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22220?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
nitin
2026-06-30 19:03:04 +05:30
committed by GitHub
parent 46ef8a8813
commit 5a5c829129
15 changed files with 459 additions and 27 deletions
@@ -1,8 +1,10 @@
import { createContext } from 'react';
import { type RecordFilterValueDependencies } from 'twenty-shared/types';
export type RecordFilterValueDependenciesContextValue = {
currentRecordId?: string;
};
export type RecordFilterValueDependenciesContextValue = Pick<
RecordFilterValueDependencies,
'currentRecord'
>;
export const RecordFilterValueDependenciesContext =
createContext<RecordFilterValueDependenciesContextValue>({});
@@ -14,15 +14,15 @@ export const useFilterValueDependencies = (): {
const { userTimezone } = useUserTimezone();
const { currentRecordId } = useContext(RecordFilterValueDependenciesContext);
const { currentRecord } = useContext(RecordFilterValueDependenciesContext);
const filterValueDependencies = useMemo(
() => ({
currentWorkspaceMemberId,
currentRecordId,
currentRecord,
timeZone: userTimezone,
}),
[currentWorkspaceMemberId, currentRecordId, userTimezone],
[currentWorkspaceMemberId, currentRecord, userTimezone],
);
return { filterValueDependencies };
@@ -24,7 +24,7 @@ export const useBuildRecordInputFromFilters = ({
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const { currentRecordId } = useContext(RecordFilterValueDependenciesContext);
const { currentRecord } = useContext(RecordFilterValueDependenciesContext);
const { userTimezone } = useUserTimezone();
@@ -33,7 +33,7 @@ export const useBuildRecordInputFromFilters = ({
currentRecordFilters,
objectMetadataItem,
currentWorkspaceMember: currentWorkspaceMember ?? undefined,
currentRecordId,
currentRecordId: currentRecord?.id,
timeZone: userTimezone,
});
};
@@ -1,3 +1,4 @@
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { UPSERT_VIEW_WIDGET } from '@/page-layout/graphql/mutations/upsertViewWidget';
import { useHasRecordTableWidgetViewChanges } from '@/page-layout/hooks/useHasRecordTableWidgetViewChanges';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -5,6 +6,7 @@ import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/r
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
import { widgetUsesRecordTableView } from '@/page-layout/utils/widgetUsesRecordTableView';
import { normalizeRecordTableWidgetViewFields } from '@/page-layout/widgets/record-table/utils/normalizeRecordTableWidgetViewFields';
import { useMutation } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@@ -47,6 +49,10 @@ export const useSaveRecordTableWidgetViews = () => {
.flatMap((tab) => tab.widgets)
.filter(widgetUsesRecordTableView);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
let normalizedRecordTableWidgetViewDraft = recordTableWidgetViewDraft;
for (const widget of draftRecordTableWidgets) {
const viewId = getWidgetConfigurationViewId(widget.configuration);
@@ -60,11 +66,34 @@ export const useSaveRecordTableWidgetViews = () => {
continue;
}
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === widgetViewDraft.view.objectMetadataId,
);
const normalizedViewFields = isDefined(objectMetadataItem)
? normalizeRecordTableWidgetViewFields({
viewFields: widgetViewDraft.viewFields,
labelIdentifierFieldMetadataId:
objectMetadataItem.labelIdentifierFieldMetadataId,
})
: widgetViewDraft.viewFields;
const normalizedWidgetViewDraft = {
...widgetViewDraft,
viewFields: normalizedViewFields,
};
normalizedRecordTableWidgetViewDraft = {
...normalizedRecordTableWidgetViewDraft,
[widget.id]: normalizedWidgetViewDraft,
};
await upsertViewWidgetMutation({
variables: {
input: {
widgetId: widget.id,
viewFields: widgetViewDraft.viewFields.map((field) => ({
viewFields: normalizedWidgetViewDraft.viewFields.map((field) => ({
fieldMetadataId: field.fieldMetadataId,
isVisible: field.isVisible,
position: field.position,
@@ -100,11 +129,18 @@ export const useSaveRecordTableWidgetViews = () => {
});
}
store.set(
recordTableWidgetViewDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
normalizedRecordTableWidgetViewDraft,
);
store.set(
recordTableWidgetViewPersistedComponentState.atomFamily({
instanceId: pageLayoutId,
}),
recordTableWidgetViewDraft,
normalizedRecordTableWidgetViewDraft,
);
},
[hasRecordTableWidgetViewChanges, store, upsertViewWidgetMutation],
@@ -41,14 +41,25 @@ export const FieldWidgetRelationTable = ({
const relationObjectMetadataId =
fieldDefinition.metadata.relationObjectMetadataId;
const recordPageObjectMetadataNameSingular =
fieldDefinition.metadata.objectMetadataNameSingular;
if (!isDefined(viewId) || !isDefined(relationObjectMetadataId)) {
if (
!isDefined(viewId) ||
!isDefined(relationObjectMetadataId) ||
!isDefined(recordPageObjectMetadataNameSingular)
) {
return null;
}
return (
<RecordFilterValueDependenciesContext.Provider
value={{ currentRecordId: recordId }}
value={{
currentRecord: {
id: recordId,
objectMetadataNameSingular: recordPageObjectMetadataNameSingular,
},
}}
>
<StyledContainer>
<RecordTableWidgetRendererContent
@@ -0,0 +1,68 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useAddDraftViewForFieldRelationTableWidget } from '@/page-layout/widgets/record-table/hooks/useAddDraftViewForFieldRelationTableWidget';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
FieldDisplayMode,
type FieldConfiguration,
RelationType,
} from '~/generated-metadata/graphql';
type ResolveFieldWidgetRelationTableViewIdChangeArgs = {
selectedField: FieldMetadataItem | undefined;
currentDisplayMode: FieldDisplayMode | undefined;
isSelectingDifferentField: boolean;
widgetId: string | undefined;
currentViewId: string | null | undefined;
};
export const useResolveFieldWidgetRelationTableViewIdChange = (
pageLayoutId: string,
) => {
const { addDraftViewForFieldRelationTableWidget } =
useAddDraftViewForFieldRelationTableWidget(pageLayoutId);
const resolveFieldWidgetRelationTableViewIdChange = ({
selectedField,
currentDisplayMode,
isSelectingDifferentField,
widgetId,
currentViewId,
}: ResolveFieldWidgetRelationTableViewIdChangeArgs):
| Pick<FieldConfiguration, 'viewId'>
| undefined => {
const targetObjectMetadataId =
selectedField?.relation?.targetObjectMetadata.id;
const targetFieldMetadataId =
selectedField?.relation?.targetFieldMetadata.id;
const shouldRegenerateRelationTableView =
currentDisplayMode === FieldDisplayMode.TABLE &&
isSelectingDifferentField &&
selectedField?.type === FieldMetadataType.RELATION &&
selectedField.relation?.type === RelationType.ONE_TO_MANY &&
isDefined(widgetId) &&
isDefined(targetObjectMetadataId) &&
isDefined(targetFieldMetadataId);
const regeneratedRelationTableViewId = shouldRegenerateRelationTableView
? addDraftViewForFieldRelationTableWidget(
widgetId,
targetObjectMetadataId,
targetFieldMetadataId,
)
: undefined;
if (isDefined(regeneratedRelationTableViewId)) {
return { viewId: regeneratedRelationTableViewId };
}
if (isSelectingDifferentField && isDefined(currentViewId)) {
return { viewId: undefined };
}
return undefined;
};
return { resolveFieldWidgetRelationTableViewIdChange };
};
@@ -10,4 +10,28 @@ describe('buildRecordTableWidgetViewSnapshot', () => {
expect(view.visibility).toBe(ViewVisibility.WORKSPACE);
});
it('should keep the label identifier view field visible and in the lowest position', () => {
const { viewFields } =
buildRecordTableWidgetViewSnapshot(objectMetadataItem);
const labelIdentifierViewField = viewFields.find(
(viewField) =>
viewField.fieldMetadataId ===
objectMetadataItem.labelIdentifierFieldMetadataId,
);
expect(labelIdentifierViewField).toMatchObject({
isVisible: true,
position: 0,
});
expect(
viewFields.every(
(viewField) =>
viewField.fieldMetadataId ===
objectMetadataItem.labelIdentifierFieldMetadataId ||
viewField.position > 0,
),
).toBe(true);
});
});
@@ -0,0 +1,75 @@
import { normalizeRecordTableWidgetViewFields } from '@/page-layout/widgets/record-table/utils/normalizeRecordTableWidgetViewFields';
describe('normalizeRecordTableWidgetViewFields', () => {
it('should keep the label identifier view field visible and before every other view field', () => {
const normalizedViewFields = normalizeRecordTableWidgetViewFields({
labelIdentifierFieldMetadataId: 'label-field-id',
viewFields: [
{
id: 'hidden-field-id',
fieldMetadataId: 'hidden-field-metadata-id',
isVisible: false,
position: 0,
size: 180,
},
{
id: 'label-view-field-id',
fieldMetadataId: 'label-field-id',
isVisible: false,
position: 4,
size: 180,
},
{
id: 'visible-field-id',
fieldMetadataId: 'visible-field-metadata-id',
isVisible: true,
position: 0,
size: 180,
},
],
});
expect(normalizedViewFields).toEqual([
{
id: 'label-view-field-id',
fieldMetadataId: 'label-field-id',
isVisible: true,
position: 0,
size: 180,
},
{
id: 'hidden-field-id',
fieldMetadataId: 'hidden-field-metadata-id',
isVisible: false,
position: 1,
size: 180,
},
{
id: 'visible-field-id',
fieldMetadataId: 'visible-field-metadata-id',
isVisible: true,
position: 2,
size: 180,
},
]);
});
it('should leave view fields unchanged when the label identifier view field is absent', () => {
const viewFields = [
{
id: 'field-id',
fieldMetadataId: 'field-metadata-id',
isVisible: false,
position: 0,
size: 180,
},
];
expect(
normalizeRecordTableWidgetViewFields({
viewFields,
labelIdentifierFieldMetadataId: 'missing-label-field-id',
}),
).toBe(viewFields);
});
});
@@ -3,6 +3,7 @@ import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
import { normalizeRecordTableWidgetViewFields } from '@/page-layout/widgets/record-table/utils/normalizeRecordTableWidgetViewFields';
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
import { v4 } from 'uuid';
import {
@@ -46,15 +47,19 @@ export const buildRecordTableWidgetViewSnapshot = (
),
);
const flatViewFields: FlatViewField[] = sortedFields.map((field, index) => ({
id: v4(),
viewId: newViewId,
fieldMetadataId: field.id,
position: index,
size: DEFAULT_VIEW_FIELD_SIZE,
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
isActive: true,
}));
const flatViewFields: FlatViewField[] = normalizeRecordTableWidgetViewFields({
viewFields: sortedFields.map((field, index) => ({
id: v4(),
viewId: newViewId,
fieldMetadataId: field.id,
position: index,
size: DEFAULT_VIEW_FIELD_SIZE,
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
isActive: true,
})),
labelIdentifierFieldMetadataId:
objectMetadataItem.labelIdentifierFieldMetadataId,
});
return {
view: flatView,
@@ -0,0 +1,50 @@
import { isDefined } from 'twenty-shared/utils';
type RecordTableWidgetViewFieldWithLabelIdentifierInvariant = {
fieldMetadataId: string;
isVisible: boolean;
position: number;
};
type NormalizeRecordTableWidgetViewFieldsArgs<
ViewField extends RecordTableWidgetViewFieldWithLabelIdentifierInvariant,
> = {
viewFields: ViewField[];
labelIdentifierFieldMetadataId: string;
};
export const normalizeRecordTableWidgetViewFields = <
ViewField extends RecordTableWidgetViewFieldWithLabelIdentifierInvariant,
>({
viewFields,
labelIdentifierFieldMetadataId,
}: NormalizeRecordTableWidgetViewFieldsArgs<ViewField>): ViewField[] => {
const labelIdentifierViewField = viewFields.find(
(viewField) => viewField.fieldMetadataId === labelIdentifierFieldMetadataId,
);
if (!isDefined(labelIdentifierViewField)) {
return viewFields;
}
const normalizedLabelIdentifierViewField: ViewField = {
...labelIdentifierViewField,
isVisible: true,
position: 0,
};
const normalizedOtherViewFields = viewFields
.filter(
(viewField) =>
viewField.fieldMetadataId !== labelIdentifierFieldMetadataId,
)
.toSorted((viewFieldA, viewFieldB) => {
return viewFieldA.position - viewFieldB.position;
})
.map<ViewField>((viewField, index) => ({
...viewField,
position: index + 1,
}));
return [normalizedLabelIdentifierViewField, ...normalizedOtherViewFields];
};
@@ -1,8 +1,8 @@
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { isAdvancedRelationFieldMetadataItem } from '@/object-record/utils/isAdvancedRelationFieldMetadataItem';
import { isDefined } from 'twenty-shared/utils';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { useResolveFieldWidgetRelationTableViewIdChange } from '@/page-layout/widgets/record-table/hooks/useResolveFieldWidgetRelationTableViewIdChange';
import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields';
import {
getFieldWidgetDefaultDisplayMode,
@@ -26,6 +26,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { useMemo, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/icon';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type FieldConfiguration } from '~/generated-metadata/graphql';
@@ -92,6 +93,9 @@ export const FieldWidgetFieldDropdownContent = () => {
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
const { resolveFieldWidgetRelationTableViewIdChange } =
useResolveFieldWidgetRelationTableViewIdChange(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const { getIcon } = useIcons();
@@ -126,9 +130,22 @@ export const FieldWidgetFieldDropdownContent = () => {
selectedField.relation?.type,
);
const isSelectingDifferentField =
currentFieldMetadataId !== fieldMetadataId;
const relationTableViewIdChange =
resolveFieldWidgetRelationTableViewIdChange({
selectedField,
currentDisplayMode,
isSelectingDifferentField,
widgetId: widgetInEditMode?.id,
currentViewId: fieldConfiguration?.viewId,
});
updateCurrentWidgetConfig({
configToUpdate: {
fieldMetadataId,
...relationTableViewIdChange,
...(needsDisplayModeSwitch && {
fieldDisplayMode: getFieldWidgetDefaultDisplayMode(
selectedField.type,
@@ -1,5 +1,8 @@
export interface RecordFilterValueDependencies {
currentWorkspaceMemberId?: string;
currentRecordId?: string;
currentRecord?: {
id: string;
objectMetadataNameSingular: string;
};
timeZone?: string;
}
@@ -1,5 +1,6 @@
import {
FieldMetadataType,
RelationType,
ViewFilterOperand as RecordFilterOperand,
} from '@/types';
import { type RecordFilter } from '@/utils';
@@ -55,6 +56,28 @@ const fields = [
type: FieldMetadataType.RELATION,
label: 'Account Owner',
},
{
id: 'f-morph-relation',
name: 'target',
type: FieldMetadataType.MORPH_RELATION,
label: 'Target',
morphRelations: [
{
type: RelationType.MANY_TO_ONE,
targetObjectMetadata: {
nameSingular: 'person',
namePlural: 'people',
},
},
{
type: RelationType.MANY_TO_ONE,
targetObjectMetadata: {
nameSingular: 'company',
namePlural: 'companies',
},
},
],
},
{
id: 'f-bool',
name: 'isActive',
@@ -614,6 +637,67 @@ describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
expect(result).toHaveProperty('or');
});
it('should resolve programmatic current-record morph relation-table filters to the matching join column', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies: {
...filterValueDependencies,
currentRecord: {
id: '11111111-1111-4111-8111-111111111111',
objectMetadataNameSingular: 'person',
},
},
recordFilter: makeFilter(
'f-morph-relation',
RecordFilterOperand.IS,
JSON.stringify({
selectedRecordIds: [],
isCurrentRecordSelected: true,
}),
'RELATION',
),
fieldMetadataItemById,
});
expect(result).toEqual({
targetPersonId: { in: ['11111111-1111-4111-8111-111111111111'] },
});
});
it('should omit programmatic current-record morph filters when morph relation metadata is missing', () => {
const fieldMetadataItemByIdWithoutMorphRelations = new Map(
fieldMetadataItemById,
);
fieldMetadataItemByIdWithoutMorphRelations.set('f-morph-relation', {
id: 'f-morph-relation',
name: 'target',
type: FieldMetadataType.MORPH_RELATION,
label: 'Target',
});
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies: {
...filterValueDependencies,
currentRecord: {
id: '11111111-1111-4111-8111-111111111111',
objectMetadataNameSingular: 'person',
},
},
recordFilter: makeFilter(
'f-morph-relation',
RecordFilterOperand.IS,
JSON.stringify({
selectedRecordIds: [],
isCurrentRecordSelected: true,
}),
'RELATION',
),
fieldMetadataItemById: fieldMetadataItemByIdWithoutMorphRelations,
});
expect(result).toBeUndefined();
});
});
describe('RAW_JSON filter', () => {
@@ -19,6 +19,7 @@ import {
type RawJsonFilter,
type RecordFilterValueDependencies,
type RecordGqlOperationFilter,
type RelationType,
type RelationFilter,
type SelectFilter,
type StringFilter,
@@ -54,12 +55,25 @@ import {
import { arrayOfStringsOrVariablesSchema } from '@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema';
import { arrayOfUuidOrVariableSchema } from '@/utils/filter/utils/validation-schemas/arrayOfUuidsOrVariablesSchema';
import { jsonRelationFilterValueSchema } from '@/utils/filter/utils/validation-schemas/jsonRelationFilterValueSchema';
import {
computeMorphRelationGqlFieldJoinColumnName,
computeRelationGqlFieldJoinColumnName,
} from '@/utils/fieldMetadata/compute-relation-gql-field-join-column-name';
type FieldSharedMorphRelation = {
type: RelationType;
targetObjectMetadata: {
nameSingular: string;
namePlural: string;
};
};
export type FieldShared = {
id: string;
name: string;
type: FieldMetadataType;
label: string;
morphRelations?: FieldSharedMorphRelation[] | null;
};
type TurnRecordFilterIntoRecordGqlOperationFilterParams = {
@@ -131,6 +145,39 @@ type BuildDirectFieldGqlOperationFilterParams = {
fieldMetadataItem: FieldShared;
};
const getRelationFilterJoinColumnName = ({
fieldMetadataItem,
filterValueDependencies,
}: {
fieldMetadataItem: FieldShared;
filterValueDependencies: RecordFilterValueDependencies;
}): string | undefined => {
if (fieldMetadataItem.type !== FieldMetadataType.MORPH_RELATION) {
return computeRelationGqlFieldJoinColumnName({
name: fieldMetadataItem.name,
});
}
const matchingMorphRelation = fieldMetadataItem.morphRelations?.find(
(morphRelation) =>
morphRelation.targetObjectMetadata.nameSingular ===
filterValueDependencies.currentRecord?.objectMetadataNameSingular,
);
if (!isDefined(matchingMorphRelation)) {
return;
}
return computeMorphRelationGqlFieldJoinColumnName({
fieldName: fieldMetadataItem.name,
relationType: matchingMorphRelation.type,
targetObjectMetadataNameSingular:
matchingMorphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
matchingMorphRelation.targetObjectMetadata.namePlural,
});
};
const buildDirectFieldGqlOperationFilter = ({
recordFilter,
fieldMetadataItem,
@@ -590,16 +637,25 @@ const buildDirectFieldGqlOperationFilter = ({
? [filterValueDependencies?.currentWorkspaceMemberId]
: []),
...(isCurrentRecordSelected
? [filterValueDependencies?.currentRecordId]
? [filterValueDependencies.currentRecord?.id]
: []),
].filter(isDefined);
if (recordIds.length === 0) return;
const relationFilterJoinColumnName = getRelationFilterJoinColumnName({
fieldMetadataItem,
filterValueDependencies,
});
if (!isDefined(relationFilterJoinColumnName)) {
return;
}
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
return {
[fieldMetadataItem.name + 'Id']: {
[relationFilterJoinColumnName]: {
in: recordIds,
} as RelationFilter,
};
@@ -609,13 +665,13 @@ const buildDirectFieldGqlOperationFilter = ({
or: [
{
not: {
[fieldMetadataItem.name + 'Id']: {
[relationFilterJoinColumnName]: {
in: recordIds,
} as RelationFilter,
},
},
{
[fieldMetadataItem.name + 'Id']: {
[relationFilterJoinColumnName]: {
is: 'NULL',
} as RelationFilter,
},
@@ -24,6 +24,7 @@ export const getFilterTypeFromFieldType = (
case FieldMetadataType.PHONES:
return 'PHONES';
case FieldMetadataType.RELATION:
case FieldMetadataType.MORPH_RELATION:
return 'RELATION';
case FieldMetadataType.SELECT:
return 'SELECT';