feat(server): convert view to overridable entity (#21436)

## Context

Every entity created as a side effect of object creation must support
the overridable pattern (`isActive` + `overrides` + override routing)
before we can re-own side effects to their true application. Starting
with View.

viewField, viewFieldGroup, pageLayoutTab and pageLayoutWidget already
extend `OverridableEntity`. This PR brings `view` to the same pattern.

## What this does

- `ViewEntity` now extends `OverridableEntity<ViewOverrides>` (adds
`isActive` boolean + `overrides` jsonb). All editable view properties
are overridable; the 3 fieldMetadata foreign keys are converted to/from
universal identifiers like viewField's `viewFieldGroupId`.
- **Update**: mutations on a view not owned by the caller (e.g. standard
views like "All Companies") are written into `overrides` instead of
mutating the row. Reads merge overrides in
the DTO.
- **Delete/destroy**: views not owned by the caller are deactivated
(`isActive = false`) instead of deleted.
~~- **INDEX invariant**: `key = INDEX` views can only be created via
object-creation side effect. The API now rejects creating, deleting or
destroying INDEX views (object-deletion cascade is unaffected). This was
not really needed for this migration but was flagged during
implementation.~~
- **Front**: views with `isActive = false` are filtered out of the views
selector.
- Fast instance command adds the two columns
(`2-12-instance-command-fast-...-view-overridable-entity.ts`).

## Notes

- Custom (caller-owned) views behave exactly as before: direct updates,
soft delete.
- View-group side effects (kanban groups) are computed on the
override-merged view so overridden `mainGroupByFieldMetadataId` works.
This commit is contained in:
Weiko
2026-06-11 18:00:52 +02:00
committed by GitHub
parent 947a4d5253
commit cfb9772179
35 changed files with 497 additions and 52 deletions
@@ -877,6 +877,7 @@ type View {
viewFieldGroups: [ViewFieldGroup!]!
visibility: ViewVisibility!
createdByUserWorkspaceId: UUID
isActive: Boolean!
}
enum ViewType {
@@ -615,6 +615,7 @@ export interface View {
viewFieldGroups: ViewFieldGroup[]
visibility: ViewVisibility
createdByUserWorkspaceId?: Scalars['UUID']
isActive: Scalars['Boolean']
__typename: 'View'
}
@@ -3552,6 +3553,7 @@ export interface ViewGenqlSelection{
viewFieldGroups?: ViewFieldGroupGenqlSelection
visibility?: boolean | number
createdByUserWorkspaceId?: boolean | number
isActive?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
@@ -1739,6 +1739,9 @@ export default {
"createdByUserWorkspaceId": [
3
],
"isActive": [
6
],
"__typename": [
1
]
@@ -22,6 +22,7 @@ const FIND_ALL_VIEWS_QUERY = `
calendarLayout
visibility
createdByUserWorkspaceId
isActive
viewFields {
id
fieldMetadataId
File diff suppressed because one or more lines are too long
@@ -76,6 +76,7 @@ const renderHooks = ({
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
shouldHideEmptyGroups: false,
isActive: true,
},
]);
} else {
@@ -204,6 +204,7 @@ const createView = (
viewSorts: [],
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
...overrides,
});
@@ -30,6 +30,7 @@ export const buildRecordTableWidgetViewSnapshot = (
openRecordIn: ViewOpenRecordIn.RECORD_PAGE,
visibility: ViewVisibility.UNLISTED,
shouldHideEmptyGroups: false,
isActive: true,
};
const eligibleFields = objectMetadataItem.fields.filter(
@@ -33,6 +33,7 @@ export const VIEW_FRAGMENT = gql`
calendarLayout
visibility
createdByUserWorkspaceId
isActive
viewFields {
...ViewFieldFragment
}
@@ -14,7 +14,8 @@ import { resolveViewNamePlaceholders } from '@/views/utils/resolveViewNamePlaceh
export const viewsSelector = createAtomSelector<ViewWithRelations[]>({
key: 'viewsSelector',
get: ({ get }) => {
const flatViews = get(metadataStoreState, 'views').current as FlatView[];
const allFlatViews = get(metadataStoreState, 'views').current as FlatView[];
const flatViews = allFlatViews.filter((view) => view.isActive);
const flatObjectMetadataItems = get(
metadataStoreState,
'objectMetadataItems',
@@ -38,4 +38,5 @@ export type View = {
anyFieldFilterValue?: string | null;
visibility: ViewVisibility;
createdByUserWorkspaceId?: string | null;
isActive: boolean;
};
@@ -43,6 +43,7 @@ describe('mapRecordFilterGroupToViewFilterGroup', () => {
kanbanAggregateOperationFieldMetadataId: '',
position: 0,
visibility: ViewVisibility.WORKSPACE,
isActive: true,
};
it('should correctly map single record filter group', () => {
@@ -24,6 +24,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Calendar Channel Event Associations",
"viewFields": [
@@ -99,6 +100,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Connected Accounts",
"viewFields": [
@@ -186,6 +188,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Employment History Record Page Fields",
"viewFields": [
@@ -261,6 +264,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Task Targets",
"viewFields": [
@@ -348,6 +352,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Companies",
"viewFields": [
@@ -471,6 +476,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All People",
"viewFields": [
@@ -618,6 +624,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Opportunities",
"viewFields": [
@@ -717,6 +724,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Notes",
"viewFields": [
@@ -804,6 +812,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Tasks",
"viewFields": [
@@ -927,6 +936,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Workflows",
"viewFields": [
@@ -1026,6 +1036,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Versions",
"viewFields": [
@@ -1113,6 +1124,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Runs",
"viewFields": [
@@ -1176,6 +1188,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Dashboards",
"viewFields": [
@@ -1251,6 +1264,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Timeline Activities",
"viewFields": [
@@ -1422,6 +1436,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": "501e9e6f-e1c3-49fa-80c5-50ce232121fa",
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Calendar events",
"viewFields": [
@@ -1545,6 +1560,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Messages",
"viewFields": [
@@ -1656,6 +1672,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Message Threads",
"viewFields": [
@@ -1731,6 +1748,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Workspace Members",
"viewFields": [
@@ -1806,6 +1824,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Employment Histories",
"viewFields": [
@@ -1893,6 +1912,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Calendar Channels",
"viewFields": [
@@ -1992,6 +2012,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Attachments",
"viewFields": [
@@ -2151,6 +2172,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Calendar event participants",
"viewFields": [
@@ -2274,6 +2296,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Pet Record Page Fields",
"viewFields": [
@@ -2349,6 +2372,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Blocklists",
"viewFields": [
@@ -2412,6 +2436,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Pet Care Agreements",
"viewFields": [
@@ -2499,6 +2524,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Message Folders",
"viewFields": [
@@ -2586,6 +2612,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Message Channel Message Association Message Folders",
"viewFields": [
@@ -2649,6 +2676,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Survey results",
"viewFields": [
@@ -2736,6 +2764,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Message Participants",
"viewFields": [
@@ -2847,6 +2876,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Message Channels",
"viewFields": [
@@ -2958,6 +2988,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Workflow Automated Triggers",
"viewFields": [
@@ -3021,6 +3052,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Message Channel Message Associations",
"viewFields": [
@@ -3108,6 +3140,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Rocket Record Page Fields",
"viewFields": [
@@ -3183,6 +3216,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Survey result Record Page Fields",
"viewFields": [
@@ -3258,6 +3292,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Rockets",
"viewFields": [
@@ -3345,6 +3380,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Note Targets",
"viewFields": [
@@ -3432,6 +3468,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Pet Care Agreement Record Page Fields",
"viewFields": [
@@ -3507,6 +3544,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "All Pets",
"viewFields": [
@@ -3594,6 +3632,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "By Status",
"viewFields": [
@@ -3712,6 +3751,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "By Stage",
"viewFields": [
@@ -3862,6 +3902,7 @@ export const mockedViews: ViewWithRelations[] =
"calendarFieldMetadataId": null,
"calendarLayout": null,
"visibility": "WORKSPACE",
"isActive": true,
"createdByUserWorkspaceId": null,
"name": "Assigned to Me",
"viewFields": [
@@ -0,0 +1,17 @@
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.12.0', 1781114009075)
export class ViewOverridableEntityFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "core"."view" ADD "overrides" jsonb');
await queryRunner.query('ALTER TABLE "core"."view" ADD "isActive" boolean NOT NULL DEFAULT true');
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "core"."view" DROP COLUMN "isActive"');
await queryRunner.query('ALTER TABLE "core"."view" DROP COLUMN "overrides"');
}
}
@@ -61,6 +61,7 @@ import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/data
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
import { DropIsCustomFromObjectAndFieldMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-12/2-12-instance-command-fast-1780579070012-drop-is-custom-from-object-and-field-metadata';
import { DropEmailingDomainDriverColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-11/2-11-instance-command-fast-1780926908000-drop-emailing-domain-driver-column';
import { ViewOverridableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-12/2-12-instance-command-fast-1781114009075-view-overridable-entity';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -124,4 +125,5 @@ export const INSTANCE_COMMANDS = [
EncryptNonSecretApplicationVariableSlowInstanceCommand,
DropIsCustomFromObjectAndFieldMetadataFastInstanceCommand,
DropEmailingDomainDriverColumnFastInstanceCommand,
ViewOverridableEntityFastInstanceCommand,
];
@@ -41,6 +41,8 @@ export const fromViewManifestToUniversalFlatView = ({
shouldHideEmptyGroups: viewManifest.shouldHideEmptyGroups ?? false,
anyFieldFilterValue: null,
createdByUserWorkspaceId: null,
isActive: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewFilterGroupUniversalIdentifiers: [],
@@ -327,8 +327,12 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"visibility",
"mainGroupByFieldMetadataUniversalIdentifier",
"shouldHideEmptyGroups",
"isActive",
"universalOverrides",
],
"propertiesToStringify": [
"universalOverrides",
],
"propertiesToStringify": [],
},
"viewField": {
"propertiesToCompare": [
@@ -271,64 +271,101 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
toStringify: false,
universalProperty: undefined,
},
name: { toCompare: true, toStringify: false, universalProperty: undefined },
type: { toCompare: true, toStringify: false, universalProperty: undefined },
icon: { toCompare: true, toStringify: false, universalProperty: undefined },
name: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
type: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
icon: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
position: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
isCompact: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
openRecordIn: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
kanbanAggregateOperation: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
kanbanAggregateOperationFieldMetadataId: {
toCompare: true,
toStringify: false,
universalProperty:
'kanbanAggregateOperationFieldMetadataUniversalIdentifier',
isOverridable: true,
},
anyFieldFilterValue: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
calendarLayout: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
calendarFieldMetadataId: {
toCompare: true,
toStringify: false,
universalProperty: 'calendarFieldMetadataUniversalIdentifier',
isOverridable: true,
},
visibility: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
mainGroupByFieldMetadataId: {
toCompare: true,
toStringify: false,
universalProperty: 'mainGroupByFieldMetadataUniversalIdentifier',
isOverridable: true,
},
shouldHideEmptyGroups: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
isActive: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: false,
},
overrides: {
toCompare: true,
toStringify: true,
universalProperty: 'universalOverrides',
},
objectMetadataId: {
toCompare: false,
@@ -86,6 +86,8 @@ export const fromCreateViewInputToFlatViewToCreate = ({
universalIdentifier: createViewInput.universalIdentifier ?? v4(),
visibility: createViewInput.visibility ?? ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: createdByUserWorkspaceId ?? null,
isActive: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewGroupUniversalIdentifiers: [],
@@ -6,6 +6,7 @@ import {
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
import { type DeleteViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/delete-view.input';
import {
ViewException,
@@ -16,9 +17,13 @@ import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-m
export const fromDeleteViewInputToFlatViewOrThrow = ({
deleteViewInput: rawDeleteViewInput,
flatViewMaps,
callerApplicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
}: {
deleteViewInput: DeleteViewInput;
flatViewMaps: FlatViewMaps;
callerApplicationUniversalIdentifier: string;
workspaceCustomApplicationUniversalIdentifier: string;
}): UniversalFlatView => {
const { id: viewId } = extractAndSanitizeObjectStringFields(
rawDeleteViewInput,
@@ -37,8 +42,25 @@ export const fromDeleteViewInputToFlatViewOrThrow = ({
);
}
const now = new Date().toISOString();
const shouldDeactivate = isCallerOverridingEntity({
callerApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingFlatViewToDelete.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
if (shouldDeactivate) {
return {
...existingFlatViewToDelete,
isActive: false,
updatedAt: now,
};
}
return {
...existingFlatViewToDelete,
deletedAt: new Date().toISOString(),
deletedAt: now,
};
};
@@ -12,8 +12,12 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type';
import { FLAT_VIEW_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view/constants/flat-view-editable-properties.constant';
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
import { fromViewOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-view/utils/from-view-overrides-to-universal-overrides.util';
import { handleFlatViewUpdateSideEffect } from 'src/engine/metadata-modules/flat-view/utils/handle-flat-view-update-side-effect.util';
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
import { type UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input';
import { type ViewOverrides } from 'src/engine/metadata-modules/view/entities/view.entity';
import {
ViewException,
ViewExceptionCode,
@@ -28,12 +32,16 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
flatViewGroupMaps,
flatFieldMetadataMaps,
userWorkspaceId,
callerApplicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
}: {
updateViewInput: UpdateViewInput;
flatViewMaps: FlatViewMaps;
flatViewGroupMaps: FlatViewGroupMaps;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
userWorkspaceId?: string;
callerApplicationUniversalIdentifier: string;
workspaceCustomApplicationUniversalIdentifier: string;
}): {
flatViewToUpdate: UniversalFlatView;
flatViewGroupsToDelete: UniversalFlatViewGroup[];
@@ -57,19 +65,39 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
);
}
const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields(
const editableProperties = extractAndSanitizeObjectStringFields(
rawUpdateViewInput,
FLAT_VIEW_EDITABLE_PROPERTIES,
);
const flatViewToUpdate = mergeUpdateInExistingRecord({
existing: existingFlatViewToUpdate,
properties: FLAT_VIEW_EDITABLE_PROPERTIES,
update: updatedEditableFieldProperties,
const shouldOverride = isCallerOverridingEntity({
callerApplicationUniversalIdentifier,
entityApplicationUniversalIdentifier:
existingFlatViewToUpdate.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier,
});
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'view',
existingFlatEntity: existingFlatViewToUpdate,
updatedEditableProperties: editableProperties,
shouldOverride,
});
const mergedRecord = mergeUpdateInExistingRecord({
existing: existingFlatViewToUpdate,
properties: [...FLAT_VIEW_EDITABLE_PROPERTIES],
update: updatedEditableProperties,
});
const flatViewToUpdate = {
...mergedRecord,
overrides,
} as UniversalFlatView;
if (
updatedEditableFieldProperties.kanbanAggregateOperationFieldMetadataId !==
updatedEditableProperties.kanbanAggregateOperationFieldMetadataId !==
undefined
) {
const { kanbanAggregateOperationFieldMetadataUniversalIdentifier } =
@@ -77,7 +105,7 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
metadataName: 'view',
foreignKeyValues: {
kanbanAggregateOperationFieldMetadataId:
flatViewToUpdate.kanbanAggregateOperationFieldMetadataId,
mergedRecord.kanbanAggregateOperationFieldMetadataId,
},
flatEntityMaps: { flatFieldMetadataMaps },
});
@@ -86,12 +114,12 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
kanbanAggregateOperationFieldMetadataUniversalIdentifier;
}
if (updatedEditableFieldProperties.calendarFieldMetadataId !== undefined) {
if (updatedEditableProperties.calendarFieldMetadataId !== undefined) {
const { calendarFieldMetadataUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'view',
foreignKeyValues: {
calendarFieldMetadataId: flatViewToUpdate.calendarFieldMetadataId,
calendarFieldMetadataId: mergedRecord.calendarFieldMetadataId,
},
flatEntityMaps: { flatFieldMetadataMaps },
});
@@ -100,13 +128,12 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
calendarFieldMetadataUniversalIdentifier;
}
if (updatedEditableFieldProperties.mainGroupByFieldMetadataId !== undefined) {
if (updatedEditableProperties.mainGroupByFieldMetadataId !== undefined) {
const { mainGroupByFieldMetadataUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'view',
foreignKeyValues: {
mainGroupByFieldMetadataId:
flatViewToUpdate.mainGroupByFieldMetadataId,
mainGroupByFieldMetadataId: mergedRecord.mainGroupByFieldMetadataId,
},
flatEntityMaps: { flatFieldMetadataMaps },
});
@@ -115,6 +142,18 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
mainGroupByFieldMetadataUniversalIdentifier;
}
if (isDefined(overrides)) {
flatViewToUpdate.universalOverrides = fromViewOverridesToUniversalOverrides(
{
overrides: overrides as ViewOverrides,
fieldMetadataUniversalIdentifierById:
flatFieldMetadataMaps.universalIdentifierById,
},
);
} else {
flatViewToUpdate.universalOverrides = null;
}
// If changing visibility from WORKSPACE to UNLISTED, ensure createdByUserWorkspaceId is set
// This prevents the view from disappearing for the user making the change
if (
@@ -127,10 +166,15 @@ export const fromUpdateViewInputToFlatViewToUpdateOrThrow = ({
flatViewToUpdate.createdByUserWorkspaceId = userWorkspaceId;
}
const effectiveFlatViewToUpdate = {
...mergedRecord,
...((overrides as ViewOverrides | null) ?? {}),
};
const { flatViewGroupsToDelete, flatViewGroupsToCreate } =
handleFlatViewUpdateSideEffect({
fromFlatView: existingFlatViewToUpdate,
toFlatView: flatViewToUpdate,
toFlatView: effectiveFlatViewToUpdate,
flatViewGroupMaps: flatViewGroupMaps,
flatFieldMetadataMaps: flatFieldMetadataMaps,
});
@@ -6,6 +6,7 @@ import {
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { fromViewOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-view/utils/from-view-overrides-to-universal-overrides.util';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromViewEntityToFlatView = ({
@@ -88,12 +89,23 @@ export const fromViewEntityToFlatView = ({
}
}
const universalOverrides = isDefined(viewEntity.overrides)
? fromViewOverridesToUniversalOverrides({
overrides: viewEntity.overrides,
fieldMetadataUniversalIdentifierById: Object.fromEntries(
fieldMetadataIdToUniversalIdentifierMap.entries(),
),
shouldThrowOnMissingIdentifier: false,
})
: null;
return {
...viewEntityWithoutRelations,
createdAt: viewEntity.createdAt.toISOString(),
updatedAt: viewEntity.updatedAt.toISOString(),
deletedAt: viewEntity.deletedAt?.toISOString() ?? null,
universalIdentifier: viewEntityWithoutRelations.universalIdentifier,
universalOverrides,
viewFieldIds: viewEntity.viewFields.map(({ id }) => id),
viewFieldGroupIds: viewEntity.viewFieldGroups?.map(({ id }) => id) ?? [],
viewFilterIds: viewEntity.viewFilters.map(({ id }) => id),
@@ -0,0 +1,79 @@
import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type ViewOverrides } from 'src/engine/metadata-modules/view/entities/view.entity';
type UniversalViewOverrides =
FormatRecordSerializedRelationProperties<ViewOverrides>;
const VIEW_OVERRIDES_FIELD_METADATA_FOREIGN_KEYS = [
'kanbanAggregateOperationFieldMetadataId',
'calendarFieldMetadataId',
'mainGroupByFieldMetadataId',
] as const;
type ViewOverridesFieldMetadataForeignKey =
(typeof VIEW_OVERRIDES_FIELD_METADATA_FOREIGN_KEYS)[number];
const toUniversalIdentifierProperty = (
foreignKey: ViewOverridesFieldMetadataForeignKey,
) =>
foreignKey.replace(
/Id$/,
'UniversalIdentifier',
) as keyof UniversalViewOverrides;
export const fromViewOverridesToUniversalOverrides = ({
overrides,
fieldMetadataUniversalIdentifierById,
shouldThrowOnMissingIdentifier = true,
}: {
overrides: ViewOverrides;
fieldMetadataUniversalIdentifierById: Partial<Record<string, string>>;
shouldThrowOnMissingIdentifier?: boolean;
}): UniversalViewOverrides => {
const {
kanbanAggregateOperationFieldMetadataId: _kanban,
calendarFieldMetadataId: _calendar,
mainGroupByFieldMetadataId: _mainGroupBy,
...scalarOverrides
} = overrides;
return VIEW_OVERRIDES_FIELD_METADATA_FOREIGN_KEYS.reduce<UniversalViewOverrides>(
(acc, foreignKey) => {
const foreignKeyValue = overrides[foreignKey];
if (foreignKeyValue === undefined) {
return acc;
}
const universalIdentifierProperty =
toUniversalIdentifierProperty(foreignKey);
if (foreignKeyValue === null) {
return { ...acc, [universalIdentifierProperty]: null };
}
const universalIdentifier =
fieldMetadataUniversalIdentifierById[foreignKeyValue];
if (!isDefined(universalIdentifier)) {
if (shouldThrowOnMissingIdentifier) {
throw new FlatEntityMapsException(
`FieldMetadata universal identifier not found for id: ${foreignKeyValue}`,
FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
);
}
return { ...acc, [universalIdentifierProperty]: null };
}
return { ...acc, [universalIdentifierProperty]: universalIdentifier };
},
scalarOverrides,
);
};
@@ -735,6 +735,8 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
universalIdentifier: v4(),
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
@@ -42,6 +42,8 @@ export const computeFlatRecordPageFieldsViewToCreate = ({
universalIdentifier: v4(),
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
universalOverrides: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
@@ -1,4 +1,9 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import {
Field,
HideField,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
@@ -12,6 +17,7 @@ import {
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
import { type ViewOverrides } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto';
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
@@ -117,4 +123,10 @@ export class ViewDTO {
@Field(() => UUIDScalarType, { nullable: true })
createdByUserWorkspaceId?: string | null;
@Field(() => Boolean, { nullable: false })
isActive: boolean;
@HideField()
overrides?: ViewOverrides | null;
}
@@ -14,6 +14,7 @@ import {
} from 'typeorm';
import {
AggregateOperations,
type SerializedRelation,
ViewCalendarLayout,
ViewKey,
ViewOpenRecordIn,
@@ -30,7 +31,24 @@ import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-g
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
export type ViewOverrides = {
name?: string;
type?: ViewType;
icon?: string;
position?: number;
isCompact?: boolean;
openRecordIn?: ViewOpenRecordIn;
kanbanAggregateOperation?: AggregateOperations | null;
kanbanAggregateOperationFieldMetadataId?: SerializedRelation | null;
anyFieldFilterValue?: string | null;
calendarLayout?: ViewCalendarLayout | null;
calendarFieldMetadataId?: SerializedRelation | null;
visibility?: ViewVisibility;
mainGroupByFieldMetadataId?: SerializedRelation | null;
shouldHideEmptyGroups?: boolean;
};
// We could refactor this type to be dynamic to view type
@Entity({ name: 'view', schema: 'core' })
@@ -49,7 +67,10 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
'CHK_VIEW_CALENDAR_INTEGRITY',
`("type" != 'CALENDAR' OR ("calendarLayout" IS NOT NULL AND "calendarFieldMetadataId" IS NOT NULL))`,
)
export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
export class ViewEntity
extends OverridableEntity<ViewOverrides>
implements Required<ViewEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -17,6 +17,7 @@ import { fromCreateViewInputToFlatViewToCreate } from 'src/engine/metadata-modul
import { fromDeleteViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-delete-view-input-to-flat-view-or-throw.util';
import { fromDestroyViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-destroy-view-input-to-flat-view-or-throw.util';
import { fromUpdateViewInputToFlatViewToUpdateOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-update-view-input-to-flat-view-to-update-or-throw.util';
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util';
import { fromFlatViewFieldToViewFieldDto } from 'src/engine/metadata-modules/view-field/utils/from-flat-view-field-to-view-field-dto.util';
import { fromFlatViewFilterGroupToViewFilterGroupDto } from 'src/engine/metadata-modules/view-filter-group/utils/from-flat-view-filter-group-to-view-filter-group-dto.util';
@@ -166,6 +167,10 @@ export class ViewService {
flatViewGroupMaps: existingFlatViewGroupMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
userWorkspaceId,
callerApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
});
const validateAndBuildResult =
@@ -239,6 +244,10 @@ export class ViewService {
fromDeleteViewInputToFlatViewOrThrow({
deleteViewInput,
flatViewMaps: existingFlatViewMaps,
callerApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
});
const validateAndBuildResult =
@@ -313,14 +322,29 @@ export class ViewService {
flatEntityMaps: existingFlatViewMaps,
});
const shouldDeactivate = isCallerOverridingEntity({
callerApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
entityApplicationUniversalIdentifier:
existingFlatView.applicationUniversalIdentifier,
workspaceCustomApplicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
});
const now = new Date().toISOString();
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
view: {
flatEntityToCreate: [],
flatEntityToDelete: [flatViewFromDestroyInput],
flatEntityToUpdate: [],
flatEntityToDelete: shouldDeactivate
? []
: [flatViewFromDestroyInput],
flatEntityToUpdate: shouldDeactivate
? [{ ...existingFlatView, isActive: false, updatedAt: now }]
: [],
},
},
workspaceId,
@@ -337,9 +361,17 @@ export class ViewService {
);
}
if (shouldDeactivate) {
return fromFlatViewToViewDto({
...existingFlatView,
isActive: false,
updatedAt: now,
});
}
return fromFlatViewToViewDto({
...existingFlatView,
deletedAt: new Date().toISOString(),
deletedAt: now,
});
}
@@ -2,10 +2,11 @@ import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-
import { type ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
export const fromFlatViewToViewDto = (flatView: FlatView): ViewDTO => {
const { createdAt, updatedAt, deletedAt, ...rest } = flatView;
const { createdAt, updatedAt, deletedAt, overrides, ...rest } = flatView;
return {
...rest,
...(overrides ?? {}),
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
deletedAt: deletedAt ? new Date(deletedAt) : null,
@@ -142,6 +142,9 @@ export const createStandardViewFlatMetadata = <
anyFieldFilterValue: null,
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
isActive: true,
overrides: null,
universalOverrides: null,
viewFieldIds: [],
viewFieldUniversalIdentifiers: [],
viewFieldGroupIds: [],
@@ -12,7 +12,9 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
settings: 'settings',
},
objectMetadata: {},
view: {},
view: {
overrides: 'overrides',
},
viewField: {
overrides: 'overrides',
},
@@ -64,6 +64,8 @@ type Assertions = [
| 'anyFieldFilterValue'
| 'visibility'
| 'createdByUserWorkspaceId'
| 'isActive'
| 'universalOverrides'
>
>,
];
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
@@ -10,6 +11,7 @@ import {
FlatCreateViewAction,
UniversalCreateViewAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type';
import { fromUniversalOverridesToViewOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/utils/from-universal-overrides-to-view-overrides.util';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
@@ -41,6 +43,13 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
universalForeignKeyValues: action.flatEntity,
});
const overrides = isDefined(action.flatEntity.universalOverrides)
? fromUniversalOverridesToViewOverrides({
universalOverrides: action.flatEntity.universalOverrides,
flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps,
})
: null;
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'view',
@@ -54,6 +63,7 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
kanbanAggregateOperationFieldMetadataId,
mainGroupByFieldMetadataId,
objectMetadataId,
overrides,
id: action.id ?? v4(),
applicationId: flatApplication.id,
workspaceId,
@@ -9,6 +9,7 @@ import {
FlatUpdateViewAction,
UniversalUpdateViewAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type';
import { fromUniversalOverridesToViewOverrides } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/utils/from-universal-overrides-to-view-overrides.util';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
@@ -33,11 +34,25 @@ export class UpdateViewActionHandlerService extends WorkspaceMigrationRunnerActi
universalIdentifier: action.universalIdentifier,
});
const update = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'view',
universalUpdate: action.update,
allFlatEntityMaps,
});
const { universalOverrides, ...updateWithResolvedForeignKeys } =
resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'view',
universalUpdate: action.update,
allFlatEntityMaps,
});
const update =
universalOverrides === undefined
? updateWithResolvedForeignKeys
: universalOverrides === null
? { ...updateWithResolvedForeignKeys, overrides: null }
: {
...updateWithResolvedForeignKeys,
overrides: fromUniversalOverridesToViewOverrides({
universalOverrides,
flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps,
}),
};
return {
type: 'update',
@@ -0,0 +1,66 @@
import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type ViewOverrides } from 'src/engine/metadata-modules/view/entities/view.entity';
type UniversalViewOverrides =
FormatRecordSerializedRelationProperties<ViewOverrides>;
const VIEW_OVERRIDES_UNIVERSAL_FIELD_METADATA_PROPERTIES = [
'kanbanAggregateOperationFieldMetadataUniversalIdentifier',
'calendarFieldMetadataUniversalIdentifier',
'mainGroupByFieldMetadataUniversalIdentifier',
] as const;
type ViewOverridesUniversalFieldMetadataProperty =
(typeof VIEW_OVERRIDES_UNIVERSAL_FIELD_METADATA_PROPERTIES)[number];
const toForeignKeyProperty = (
universalProperty: ViewOverridesUniversalFieldMetadataProperty,
) =>
universalProperty.replace(
/UniversalIdentifier$/,
'Id',
) as keyof ViewOverrides;
export const fromUniversalOverridesToViewOverrides = ({
universalOverrides,
flatFieldMetadataMaps,
}: {
universalOverrides: UniversalViewOverrides;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}): ViewOverrides => {
const {
kanbanAggregateOperationFieldMetadataUniversalIdentifier: _kanban,
calendarFieldMetadataUniversalIdentifier: _calendar,
mainGroupByFieldMetadataUniversalIdentifier: _mainGroupBy,
...scalarOverrides
} = universalOverrides;
return VIEW_OVERRIDES_UNIVERSAL_FIELD_METADATA_PROPERTIES.reduce<ViewOverrides>(
(acc, universalProperty) => {
const universalIdentifier = universalOverrides[universalProperty];
if (universalIdentifier === undefined) {
return acc;
}
const foreignKeyProperty = toForeignKeyProperty(universalProperty);
if (universalIdentifier === null) {
return { ...acc, [foreignKeyProperty]: null };
}
const flatFieldMetadata =
findFlatEntityByUniversalIdentifier<FlatFieldMetadata>({
flatEntityMaps: flatFieldMetadataMaps,
universalIdentifier,
});
return { ...acc, [foreignKeyProperty]: flatFieldMetadata?.id ?? null };
},
scalarOverrides,
);
};