diff --git a/packages/twenty-front/src/modules/object-record/record-filter/contexts/RecordFilterValueDependenciesContext.ts b/packages/twenty-front/src/modules/object-record/record-filter/contexts/RecordFilterValueDependenciesContext.ts index 3a83718793..43ad315a57 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/contexts/RecordFilterValueDependenciesContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/contexts/RecordFilterValueDependenciesContext.ts @@ -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({}); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterValueDependencies.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterValueDependencies.ts index 25c39a0d40..359b8b933b 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterValueDependencies.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterValueDependencies.ts @@ -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 }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts index bc50630dc2..68ebb57253 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts @@ -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, }); }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts b/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts index f44c59aa68..d692de90c4 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useSaveRecordTableWidgetViews.ts @@ -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], diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationTable.tsx b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationTable.tsx index d3ef55693e..4053ab7fcf 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationTable.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationTable.tsx @@ -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 ( { + const { addDraftViewForFieldRelationTableWidget } = + useAddDraftViewForFieldRelationTableWidget(pageLayoutId); + + const resolveFieldWidgetRelationTableViewIdChange = ({ + selectedField, + currentDisplayMode, + isSelectingDifferentField, + widgetId, + currentViewId, + }: ResolveFieldWidgetRelationTableViewIdChangeArgs): + | Pick + | 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 }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/buildRecordTableWidgetViewSnapshot.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/buildRecordTableWidgetViewSnapshot.test.ts index 365f698a3c..d5b76db6cd 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/buildRecordTableWidgetViewSnapshot.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/buildRecordTableWidgetViewSnapshot.test.ts @@ -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); + }); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/normalizeRecordTableWidgetViewFields.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/normalizeRecordTableWidgetViewFields.test.ts new file mode 100644 index 0000000000..395abcf5fe --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/__tests__/normalizeRecordTableWidgetViewFields.test.ts @@ -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); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshot.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshot.ts index 973e85c063..e438b33318 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshot.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/buildRecordTableWidgetViewSnapshot.ts @@ -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, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/normalizeRecordTableWidgetViewFields.ts b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/normalizeRecordTableWidgetViewFields.ts new file mode 100644 index 0000000000..565bb07906 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/record-table/utils/normalizeRecordTableWidgetViewFields.ts @@ -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[] => { + 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, index) => ({ + ...viewField, + position: index + 1, + })); + + return [normalizedLabelIdentifierViewField, ...normalizedOtherViewFields]; +}; diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx index a7f6ec35af..470649afcd 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx @@ -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, diff --git a/packages/twenty-shared/src/types/RecordFilterValueDependencies.ts b/packages/twenty-shared/src/types/RecordFilterValueDependencies.ts index 02642ec08b..0208b278ff 100644 --- a/packages/twenty-shared/src/types/RecordFilterValueDependencies.ts +++ b/packages/twenty-shared/src/types/RecordFilterValueDependencies.ts @@ -1,5 +1,8 @@ export interface RecordFilterValueDependencies { currentWorkspaceMemberId?: string; - currentRecordId?: string; + currentRecord?: { + id: string; + objectMetadataNameSingular: string; + }; timeZone?: string; } diff --git a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts index 95d70bf481..1c40f1b45e 100644 --- a/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts +++ b/packages/twenty-shared/src/utils/filter/__tests__/turnRecordFilterIntoGqlOperationFilter.test.ts @@ -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', () => { diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts index 14ee26ddd4..9dca576731 100644 --- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -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, }, diff --git a/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts b/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts index 23d69c54a4..046bdbc0f8 100644 --- a/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts +++ b/packages/twenty-shared/src/utils/filter/utils/getFilterTypeFromFieldType.ts @@ -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';