mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-12 09:57:03 -04:00
Add syncable NavigationMenuItem entity to core schema (#17232)
Implements a new syncable `navigationMenuItem` entity in the core schema to replace the workspace `favorite` entity. ## Next Steps - Frontend integration ([separate PR](https://github.com/twentyhq/twenty/pull/17268)) - Data migration (separate PR)
This commit is contained in:
@@ -948,6 +948,16 @@ export type CreateFrontComponentInput = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateNavigationMenuItemInput = {
|
||||
folderId?: InputMaybe<Scalars['UUID']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
targetObjectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
targetRecordId?: InputMaybe<Scalars['UUID']>;
|
||||
userWorkspaceId?: InputMaybe<Scalars['UUID']>;
|
||||
viewId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type CreateObjectInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
@@ -2000,6 +2010,7 @@ export type Mutation = {
|
||||
createFrontComponent: FrontComponent;
|
||||
createManyCoreViewFields: Array<CoreViewField>;
|
||||
createManyCoreViewGroups: Array<CoreViewGroup>;
|
||||
createNavigationMenuItem: NavigationMenuItem;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createObjectEvent: Analytics;
|
||||
createOneAgent: Agent;
|
||||
@@ -2039,6 +2050,7 @@ export type Mutation = {
|
||||
deleteFile: File;
|
||||
deleteFrontComponent: FrontComponent;
|
||||
deleteJobs: DeleteJobsResponse;
|
||||
deleteNavigationMenuItem: NavigationMenuItem;
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneCronTrigger: CronTrigger;
|
||||
deleteOneDatabaseEventTrigger: DatabaseEventTrigger;
|
||||
@@ -2127,6 +2139,7 @@ export type Mutation = {
|
||||
updateDatabaseConfigVariable: Scalars['Boolean'];
|
||||
updateFrontComponent: FrontComponent;
|
||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||
updateNavigationMenuItem: NavigationMenuItem;
|
||||
updateOneAgent: Agent;
|
||||
updateOneApplicationVariable: Scalars['Boolean'];
|
||||
updateOneCronTrigger: CronTrigger;
|
||||
@@ -2309,6 +2322,11 @@ export type MutationCreateManyCoreViewGroupsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateNavigationMenuItemArgs = {
|
||||
input: CreateNavigationMenuItemInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOidcIdentityProviderArgs = {
|
||||
input: SetupOidcSsoInput;
|
||||
};
|
||||
@@ -2504,6 +2522,11 @@ export type MutationDeleteJobsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteNavigationMenuItemArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneAgentArgs = {
|
||||
input: AgentIdInput;
|
||||
};
|
||||
@@ -2932,6 +2955,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateNavigationMenuItemArgs = {
|
||||
input: UpdateOneNavigationMenuItemInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneAgentArgs = {
|
||||
input: UpdateAgentInput;
|
||||
};
|
||||
@@ -3158,6 +3186,21 @@ export type NativeModelCapabilities = {
|
||||
webSearch?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type NavigationMenuItem = {
|
||||
__typename?: 'NavigationMenuItem';
|
||||
applicationId?: Maybe<Scalars['UUID']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
folderId?: Maybe<Scalars['UUID']>;
|
||||
id: Scalars['UUID'];
|
||||
name?: Maybe<Scalars['String']>;
|
||||
position: Scalars['Float'];
|
||||
targetObjectMetadataId?: Maybe<Scalars['UUID']>;
|
||||
targetRecordId?: Maybe<Scalars['UUID']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
userWorkspaceId?: Maybe<Scalars['UUID']>;
|
||||
viewId?: Maybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type NotesConfiguration = {
|
||||
__typename?: 'NotesConfiguration';
|
||||
configurationType: WidgetConfigurationType;
|
||||
@@ -3621,6 +3664,8 @@ export type Query = {
|
||||
indexMetadatas: IndexConnection;
|
||||
lineChartData: LineChartDataOutput;
|
||||
listPlans: Array<BillingPlanOutput>;
|
||||
navigationMenuItem?: Maybe<NavigationMenuItem>;
|
||||
navigationMenuItems: Array<NavigationMenuItem>;
|
||||
object: Object;
|
||||
objects: ObjectConnection;
|
||||
pieChartData: PieChartDataOutput;
|
||||
@@ -3953,6 +3998,11 @@ export type QueryLineChartDataArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryNavigationMenuItemArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryObjectArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -4741,6 +4791,12 @@ export type UpdateLabPublicFeatureFlagInput = {
|
||||
value: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UpdateNavigationMenuItemInput = {
|
||||
folderId?: InputMaybe<Scalars['UUID']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
export type UpdateObjectPayload = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
@@ -4762,6 +4818,13 @@ export type UpdateOneFieldMetadataInput = {
|
||||
update: UpdateFieldInput;
|
||||
};
|
||||
|
||||
export type UpdateOneNavigationMenuItemInput = {
|
||||
/** The id of the record to update */
|
||||
id: Scalars['UUID'];
|
||||
/** The record to update */
|
||||
update: UpdateNavigationMenuItemInput;
|
||||
};
|
||||
|
||||
export type UpdateOneObjectInput = {
|
||||
/** The id of the object to update */
|
||||
id: Scalars['UUID'];
|
||||
|
||||
@@ -3068,6 +3068,21 @@ export type NativeModelCapabilities = {
|
||||
webSearch?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type NavigationMenuItem = {
|
||||
__typename?: 'NavigationMenuItem';
|
||||
applicationId?: Maybe<Scalars['UUID']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
folderId?: Maybe<Scalars['UUID']>;
|
||||
id: Scalars['UUID'];
|
||||
name?: Maybe<Scalars['String']>;
|
||||
position: Scalars['Float'];
|
||||
targetObjectMetadataId?: Maybe<Scalars['UUID']>;
|
||||
targetRecordId?: Maybe<Scalars['UUID']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
userWorkspaceId?: Maybe<Scalars['UUID']>;
|
||||
viewId?: Maybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type NotesConfiguration = {
|
||||
__typename?: 'NotesConfiguration';
|
||||
configurationType: WidgetConfigurationType;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddNavigationMenuItemEntity1768807499350
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddNavigationMenuItemEntity1768807499350';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."navigationMenuItem" ("workspaceId" uuid NOT NULL, "universalIdentifier" uuid NOT NULL, "applicationId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userWorkspaceId" uuid, "targetRecordId" uuid, "targetObjectMetadataId" uuid, "viewId" uuid, "name" text, "folderId" uuid, "position" integer NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "CHK_navigation_menu_item_target_fields" CHECK (("targetRecordId" IS NULL AND "targetObjectMetadataId" IS NULL) OR ("targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL)), CONSTRAINT "PK_d8689756f55769faea7dc0ae968" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_4d8beaebdfcd5d82ebe6e8b58f" ON "core"."navigationMenuItem" ("workspaceId", "universalIdentifier") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_NAVIGATION_MENU_ITEM_VIEW_ID_WORKSPACE_ID" ON "core"."navigationMenuItem" ("viewId", "workspaceId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_NAVIGATION_MENU_ITEM_FOLDER_ID_WORKSPACE_ID" ON "core"."navigationMenuItem" ("folderId", "workspaceId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_NAVIGATION_MENU_ITEM_TARGET_RECORD_OBJ_METADATA_WS_ID" ON "core"."navigationMenuItem" ("targetRecordId", "targetObjectMetadataId", "workspaceId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_NAVIGATION_MENU_ITEM_USER_WORKSPACE_ID_WORKSPACE_ID" ON "core"."navigationMenuItem" ("userWorkspaceId", "workspaceId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "FK_03c63a0b00ddc3ade21ed0b1a80" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "FK_6fd84a774fe4ea4daa9aeeee5ed" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "FK_b2e02050a5faa58ed3e08624659" FOREIGN KEY ("userWorkspaceId") REFERENCES "core"."userWorkspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "FK_62d47d14b50b67a03f832481de7" FOREIGN KEY ("targetObjectMetadataId") REFERENCES "core"."objectMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "FK_175fc64110c36793eaf9765d1c6" FOREIGN KEY ("folderId") REFERENCES "core"."navigationMenuItem"("id") ON DELETE CASCADE ON UPDATE NO ACTION DEFERRABLE INITIALLY DEFERRED`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "FK_175fc64110c36793eaf9765d1c6"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "FK_62d47d14b50b67a03f832481de7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "FK_b2e02050a5faa58ed3e08624659"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "FK_6fd84a774fe4ea4daa9aeeee5ed"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "FK_03c63a0b00ddc3ade21ed0b1a80"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_NAVIGATION_MENU_ITEM_USER_WORKSPACE_ID_WORKSPACE_ID"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_NAVIGATION_MENU_ITEM_TARGET_RECORD_OBJ_METADATA_WS_ID"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_NAVIGATION_MENU_ITEM_FOLDER_ID_WORKSPACE_ID"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_NAVIGATION_MENU_ITEM_VIEW_ID_WORKSPACE_ID"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_4d8beaebdfcd5d82ebe6e8b58f"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."navigationMenuItem"`);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
|
||||
import { FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant';
|
||||
import { FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-navigation-menu-item/constants/flat-navigation-menu-item-editable-properties.constant';
|
||||
import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant';
|
||||
import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant';
|
||||
import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant';
|
||||
@@ -148,6 +149,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
propertiesToCompare: [...FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
navigationMenuItem: {
|
||||
propertiesToCompare: [...FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
rowLevelPermissionPredicate: {
|
||||
propertiesToCompare: [
|
||||
...FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
@@ -49,4 +50,5 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
role: RoleEntity,
|
||||
agent: AgentEntity,
|
||||
commandMenuItem: CommandMenuItemEntity,
|
||||
navigationMenuItem: NavigationMenuItemEntity,
|
||||
} as const satisfies Record<AllMetadataName, EntityTarget<ObjectLiteral>>;
|
||||
|
||||
@@ -91,6 +91,24 @@ export const ALL_METADATA_RELATIONS = {
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
navigationMenuItem: {
|
||||
manyToOne: {
|
||||
workspace: null,
|
||||
userWorkspace: null,
|
||||
application: null,
|
||||
targetObjectMetadata: {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'targetObjectMetadataId',
|
||||
},
|
||||
folder: {
|
||||
metadataName: 'navigationMenuItem',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'folderId',
|
||||
},
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
fieldMetadata: {
|
||||
manyToOne: {
|
||||
object: {
|
||||
|
||||
@@ -65,6 +65,10 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
commandMenuItem: {
|
||||
objectMetadata: true,
|
||||
},
|
||||
navigationMenuItem: {
|
||||
objectMetadata: true,
|
||||
view: true,
|
||||
},
|
||||
pageLayout: {
|
||||
objectMetadata: true,
|
||||
},
|
||||
|
||||
@@ -2,9 +2,12 @@ import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modul
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key';
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
|
||||
export type AllFlatEntityMaps = {
|
||||
[P in keyof AllFlatEntityTypesByMetadataName as MetadataToFlatEntityMapsKey<P>]: FlatEntityMaps<
|
||||
MetadataFlatEntity<P>
|
||||
>;
|
||||
} & {
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/typ
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
@@ -18,6 +19,7 @@ import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-
|
||||
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
|
||||
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
|
||||
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
|
||||
@@ -57,6 +59,11 @@ import {
|
||||
type DeleteIndexAction,
|
||||
type UpdateIndexAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/types/workspace-migration-index-action';
|
||||
import {
|
||||
type CreateNavigationMenuItemAction,
|
||||
type DeleteNavigationMenuItemAction,
|
||||
type UpdateNavigationMenuItemAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
|
||||
import {
|
||||
type CreateObjectAction,
|
||||
type DeleteObjectAction,
|
||||
@@ -310,6 +317,15 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
flatEntity: FlatCommandMenuItem;
|
||||
entity: MetadataEntity<'commandMenuItem'>;
|
||||
};
|
||||
navigationMenuItem: {
|
||||
actions: {
|
||||
create: CreateNavigationMenuItemAction;
|
||||
update: UpdateNavigationMenuItemAction;
|
||||
delete: DeleteNavigationMenuItemAction;
|
||||
};
|
||||
flatEntity: FlatNavigationMenuItem;
|
||||
entity: NavigationMenuItemEntity;
|
||||
};
|
||||
pageLayout: {
|
||||
actions: {
|
||||
create: CreatePageLayoutAction;
|
||||
|
||||
@@ -38,6 +38,13 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for navigationMenuItem 1`] = `
|
||||
[
|
||||
"objectMetadata",
|
||||
"navigationMenuItem",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for objectMetadata 1`] = `
|
||||
[
|
||||
"fieldMetadata",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
|
||||
[
|
||||
"rowLevelPermissionPredicate",
|
||||
"viewFilter",
|
||||
"navigationMenuItem",
|
||||
"pageLayoutWidget",
|
||||
"viewField",
|
||||
"commandMenuItem",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
|
||||
export const FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES = [
|
||||
'position',
|
||||
'folderId',
|
||||
'name',
|
||||
] as const satisfies (keyof FlatNavigationMenuItem)[];
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { WorkspaceFlatNavigationMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([NavigationMenuItemEntity]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [WorkspaceFlatNavigationMenuItemMapCacheService],
|
||||
exports: [WorkspaceFlatNavigationMenuItemMapCacheService],
|
||||
})
|
||||
export class FlatNavigationMenuItemModule {}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/add-flat-navigation-menu-item-to-maps-and-update-index.util';
|
||||
import { fromNavigationMenuItemEntityToFlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('flatNavigationMenuItemMaps')
|
||||
export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCacheProvider<FlatNavigationMenuItemMaps> {
|
||||
constructor(
|
||||
@InjectRepository(NavigationMenuItemEntity)
|
||||
private readonly navigationMenuItemRepository: Repository<NavigationMenuItemEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<FlatNavigationMenuItemMaps> {
|
||||
const navigationMenuItems = await this.navigationMenuItemRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const flatNavigationMenuItemMaps = {
|
||||
...createEmptyFlatEntityMaps(),
|
||||
byUserWorkspaceIdAndFolderId: {},
|
||||
};
|
||||
|
||||
for (const navigationMenuItemEntity of navigationMenuItems) {
|
||||
const flatNavigationMenuItem =
|
||||
fromNavigationMenuItemEntityToFlatNavigationMenuItem(
|
||||
navigationMenuItemEntity,
|
||||
);
|
||||
|
||||
addFlatNavigationMenuItemToMapsAndUpdateIndex({
|
||||
flatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatNavigationMenuItemMaps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
|
||||
export type FlatNavigationMenuItemMaps =
|
||||
FlatEntityMaps<FlatNavigationMenuItem> & {
|
||||
byUserWorkspaceIdAndFolderId: Partial<
|
||||
Record<string, Partial<Record<string, FlatNavigationMenuItem[]>>>
|
||||
>;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
|
||||
export type FlatNavigationMenuItem = FlatEntityFrom<NavigationMenuItemEntity>;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
export const addFlatNavigationMenuItemToMapsAndUpdateIndex = ({
|
||||
flatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
}: {
|
||||
flatNavigationMenuItem: FlatNavigationMenuItem;
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
}): void => {
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatNavigationMenuItem,
|
||||
flatEntityMapsToMutate: flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
const userWorkspaceIdKey = flatNavigationMenuItem.userWorkspaceId ?? 'null';
|
||||
const folderIdKey = flatNavigationMenuItem.folderId ?? 'null';
|
||||
|
||||
if (
|
||||
!flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[userWorkspaceIdKey]
|
||||
) {
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
userWorkspaceIdKey
|
||||
] = {};
|
||||
}
|
||||
|
||||
if (
|
||||
!flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
userWorkspaceIdKey
|
||||
][folderIdKey]
|
||||
) {
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[userWorkspaceIdKey][
|
||||
folderIdKey
|
||||
] = [];
|
||||
}
|
||||
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[userWorkspaceIdKey][
|
||||
folderIdKey
|
||||
].push(flatNavigationMenuItem);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { removeFlatNavigationMenuItemFromIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/remove-flat-navigation-menu-item-from-index.util';
|
||||
import { deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/delete-flat-entity-from-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
export const deleteFlatNavigationMenuItemFromMapsAndIndex = ({
|
||||
flatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
}: {
|
||||
flatNavigationMenuItem: FlatNavigationMenuItem;
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
}): void => {
|
||||
removeFlatNavigationMenuItemFromIndex({
|
||||
flatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow({
|
||||
entityToDeleteId: flatNavigationMenuItem.id,
|
||||
flatEntityMapsToMutate: flatNavigationMenuItemMaps,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input';
|
||||
|
||||
export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
|
||||
({
|
||||
createNavigationMenuItemInput,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
flatNavigationMenuItemMaps,
|
||||
}: {
|
||||
createNavigationMenuItemInput: CreateNavigationMenuItemInput;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
}): FlatNavigationMenuItem => {
|
||||
const id = uuidv4();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
let position = createNavigationMenuItemInput.position;
|
||||
|
||||
if (!isDefined(position)) {
|
||||
const userWorkspaceIdKey =
|
||||
createNavigationMenuItemInput.userWorkspaceId ?? 'null';
|
||||
const folderIdKey = createNavigationMenuItemInput.folderId ?? 'null';
|
||||
|
||||
const existingItems =
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
userWorkspaceIdKey
|
||||
]?.[folderIdKey] ?? [];
|
||||
|
||||
const maxPosition = existingItems.reduce(
|
||||
(max, item) => Math.max(max, item.position),
|
||||
0,
|
||||
);
|
||||
|
||||
position = maxPosition + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
universalIdentifier: id,
|
||||
userWorkspaceId: createNavigationMenuItemInput.userWorkspaceId ?? null,
|
||||
targetRecordId: createNavigationMenuItemInput.targetRecordId ?? null,
|
||||
targetObjectMetadataId:
|
||||
createNavigationMenuItemInput.targetObjectMetadataId ?? null,
|
||||
viewId: createNavigationMenuItemInput.viewId ?? null,
|
||||
folderId: createNavigationMenuItemInput.folderId ?? null,
|
||||
name: createNavigationMenuItemInput.name ?? null,
|
||||
position,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import {
|
||||
NavigationMenuItemException,
|
||||
NavigationMenuItemExceptionCode,
|
||||
} from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||
|
||||
export const fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow =
|
||||
({
|
||||
flatNavigationMenuItemMaps,
|
||||
navigationMenuItemId,
|
||||
}: {
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
navigationMenuItemId: string;
|
||||
}): FlatNavigationMenuItem => {
|
||||
const existingFlatNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: navigationMenuItemId,
|
||||
flatEntityMaps: flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatNavigationMenuItem)) {
|
||||
throw new NavigationMenuItemException(
|
||||
'Navigation menu item not found',
|
||||
NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return existingFlatNavigationMenuItem;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
|
||||
export const fromFlatNavigationMenuItemToNavigationMenuItemDto = (
|
||||
flatNavigationMenuItem: FlatNavigationMenuItem,
|
||||
): NavigationMenuItemDTO => ({
|
||||
id: flatNavigationMenuItem.id,
|
||||
userWorkspaceId: flatNavigationMenuItem.userWorkspaceId ?? undefined,
|
||||
targetRecordId: flatNavigationMenuItem.targetRecordId ?? undefined,
|
||||
targetObjectMetadataId:
|
||||
flatNavigationMenuItem.targetObjectMetadataId ?? undefined,
|
||||
viewId: flatNavigationMenuItem.viewId ?? undefined,
|
||||
folderId: flatNavigationMenuItem.folderId ?? undefined,
|
||||
name: flatNavigationMenuItem.name ?? undefined,
|
||||
position: flatNavigationMenuItem.position,
|
||||
workspaceId: flatNavigationMenuItem.workspaceId,
|
||||
applicationId: flatNavigationMenuItem.applicationId ?? undefined,
|
||||
createdAt: new Date(flatNavigationMenuItem.createdAt),
|
||||
updatedAt: new Date(flatNavigationMenuItem.updatedAt),
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
|
||||
export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = (
|
||||
navigationMenuItemEntity: NavigationMenuItemEntity,
|
||||
): FlatNavigationMenuItem => {
|
||||
return {
|
||||
id: navigationMenuItemEntity.id,
|
||||
userWorkspaceId: navigationMenuItemEntity.userWorkspaceId,
|
||||
targetRecordId: navigationMenuItemEntity.targetRecordId,
|
||||
targetObjectMetadataId: navigationMenuItemEntity.targetObjectMetadataId,
|
||||
viewId: navigationMenuItemEntity.viewId,
|
||||
folderId: navigationMenuItemEntity.folderId,
|
||||
name: navigationMenuItemEntity.name,
|
||||
position: navigationMenuItemEntity.position,
|
||||
workspaceId: navigationMenuItemEntity.workspaceId,
|
||||
universalIdentifier: navigationMenuItemEntity.universalIdentifier,
|
||||
applicationId: navigationMenuItemEntity.applicationId,
|
||||
createdAt: navigationMenuItemEntity.createdAt.toISOString(),
|
||||
updatedAt: navigationMenuItemEntity.updatedAt.toISOString(),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-navigation-menu-item/constants/flat-navigation-menu-item-editable-properties.constant';
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type UpdateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
|
||||
import {
|
||||
NavigationMenuItemException,
|
||||
NavigationMenuItemExceptionCode,
|
||||
} from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
|
||||
export const fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow =
|
||||
({
|
||||
flatNavigationMenuItemMaps,
|
||||
updateNavigationMenuItemInput,
|
||||
}: {
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
updateNavigationMenuItemInput: UpdateNavigationMenuItemInput & {
|
||||
id: string;
|
||||
};
|
||||
}): FlatNavigationMenuItem => {
|
||||
const existingFlatNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: updateNavigationMenuItemInput.id,
|
||||
flatEntityMaps: flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatNavigationMenuItem)) {
|
||||
throw new NavigationMenuItemException(
|
||||
'Navigation menu item not found',
|
||||
NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const { id: _id, ...updates } = updateNavigationMenuItemInput;
|
||||
|
||||
return {
|
||||
...mergeUpdateInExistingRecord({
|
||||
existing: existingFlatNavigationMenuItem,
|
||||
properties: [...FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES],
|
||||
update: updates,
|
||||
}),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
|
||||
export const removeFlatNavigationMenuItemFromIndex = ({
|
||||
flatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
}: {
|
||||
flatNavigationMenuItem: FlatNavigationMenuItem;
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
}): void => {
|
||||
const userWorkspaceIdKey = flatNavigationMenuItem.userWorkspaceId ?? 'null';
|
||||
const folderIdKey = flatNavigationMenuItem.folderId ?? 'null';
|
||||
|
||||
const itemsArray =
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
userWorkspaceIdKey
|
||||
]?.[folderIdKey];
|
||||
|
||||
if (!itemsArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = itemsArray.findIndex(
|
||||
(item) => item.id === flatNavigationMenuItem.id,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
itemsArray.splice(index, 1);
|
||||
|
||||
if (itemsArray.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userWorkspaceMap =
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[userWorkspaceIdKey];
|
||||
|
||||
if (!userWorkspaceMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete userWorkspaceMap[folderIdKey];
|
||||
|
||||
if (Object.keys(userWorkspaceMap).length === 0) {
|
||||
delete flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
userWorkspaceIdKey
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { removeFlatNavigationMenuItemFromIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/remove-flat-navigation-menu-item-from-index.util';
|
||||
import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
export const replaceFlatNavigationMenuItemInMapsAndUpdateIndex = ({
|
||||
fromFlatNavigationMenuItem,
|
||||
toFlatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
}: {
|
||||
fromFlatNavigationMenuItem: FlatNavigationMenuItem;
|
||||
toFlatNavigationMenuItem: FlatNavigationMenuItem;
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
}): void => {
|
||||
const oldUserWorkspaceIdKey =
|
||||
fromFlatNavigationMenuItem.userWorkspaceId ?? 'null';
|
||||
const oldFolderIdKey = fromFlatNavigationMenuItem.folderId ?? 'null';
|
||||
const newUserWorkspaceIdKey =
|
||||
toFlatNavigationMenuItem.userWorkspaceId ?? 'null';
|
||||
const newFolderIdKey = toFlatNavigationMenuItem.folderId ?? 'null';
|
||||
|
||||
const groupChanged =
|
||||
oldUserWorkspaceIdKey !== newUserWorkspaceIdKey ||
|
||||
oldFolderIdKey !== newFolderIdKey;
|
||||
|
||||
if (groupChanged) {
|
||||
removeFlatNavigationMenuItemFromIndex({
|
||||
flatNavigationMenuItem: fromFlatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps,
|
||||
});
|
||||
}
|
||||
|
||||
replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: toFlatNavigationMenuItem,
|
||||
flatEntityMapsToMutate: flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (groupChanged) {
|
||||
if (
|
||||
!flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
newUserWorkspaceIdKey
|
||||
]
|
||||
) {
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
newUserWorkspaceIdKey
|
||||
] = {};
|
||||
}
|
||||
|
||||
if (
|
||||
!flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
newUserWorkspaceIdKey
|
||||
][newFolderIdKey]
|
||||
) {
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
newUserWorkspaceIdKey
|
||||
][newFolderIdKey] = [];
|
||||
}
|
||||
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
newUserWorkspaceIdKey
|
||||
][newFolderIdKey].push(toFlatNavigationMenuItem);
|
||||
} else {
|
||||
const itemsArray =
|
||||
flatNavigationMenuItemMaps.byUserWorkspaceIdAndFolderId[
|
||||
newUserWorkspaceIdKey
|
||||
]?.[newFolderIdKey];
|
||||
|
||||
if (!itemsArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = itemsArray.findIndex(
|
||||
(item) => item.id === toFlatNavigationMenuItem.id,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedItem =
|
||||
flatNavigationMenuItemMaps.byId[toFlatNavigationMenuItem.id];
|
||||
|
||||
if (!isDefined(updatedItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
itemsArray[index] = updatedItem;
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { AiAgentMonitorModule } from 'src/engine/metadata-modules/ai/ai-agent-mo
|
||||
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
|
||||
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
|
||||
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
|
||||
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
|
||||
import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
|
||||
@@ -31,6 +32,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
ServerlessFunctionLayerModule,
|
||||
SkillModule,
|
||||
CommandMenuItemModule,
|
||||
NavigationMenuItemModule,
|
||||
AiAgentModule,
|
||||
AiAgentMonitorModule,
|
||||
AiChatModule,
|
||||
@@ -52,6 +54,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
ServerlessFunctionModule,
|
||||
SkillModule,
|
||||
CommandMenuItemModule,
|
||||
NavigationMenuItemModule,
|
||||
AiAgentModule,
|
||||
AiChatModule,
|
||||
ViewModule,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||
|
||||
import { IsInt, IsOptional, IsString, IsUUID, Min } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class CreateNavigationMenuItemInput {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
userWorkspaceId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
targetRecordId?: string | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
targetObjectMetadataId?: string | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
viewId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field(() => String, { nullable: true })
|
||||
name?: string | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
folderId?: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@Field(() => Int, { nullable: true })
|
||||
position?: number;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsDateString,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('NavigationMenuItem')
|
||||
export class NavigationMenuItemDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
userWorkspaceId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
targetRecordId?: string | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
targetObjectMetadataId?: string | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
viewId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
name?: string | null;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
folderId?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
position: number;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
applicationId?: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateNavigationMenuItemInput {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
folderId?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@Field(() => Int, { nullable: true })
|
||||
position?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field(() => String, { nullable: true })
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateOneNavigationMenuItemInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'The id of the record to update',
|
||||
})
|
||||
id!: string;
|
||||
|
||||
@Type(() => UpdateNavigationMenuItemInput)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateNavigationMenuItemInput, {
|
||||
description: 'The record to update',
|
||||
})
|
||||
update!: UpdateNavigationMenuItemInput;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Check,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
type Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity({ name: 'navigationMenuItem', schema: 'core' })
|
||||
@Index('IDX_NAVIGATION_MENU_ITEM_USER_WORKSPACE_ID_WORKSPACE_ID', [
|
||||
'userWorkspaceId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_NAVIGATION_MENU_ITEM_TARGET_RECORD_OBJ_METADATA_WS_ID', [
|
||||
'targetRecordId',
|
||||
'targetObjectMetadataId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_NAVIGATION_MENU_ITEM_FOLDER_ID_WORKSPACE_ID', [
|
||||
'folderId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Index('IDX_NAVIGATION_MENU_ITEM_VIEW_ID_WORKSPACE_ID', [
|
||||
'viewId',
|
||||
'workspaceId',
|
||||
])
|
||||
@Check(
|
||||
'CHK_navigation_menu_item_target_fields',
|
||||
'("targetRecordId" IS NULL AND "targetObjectMetadataId" IS NULL) OR ("targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL)',
|
||||
)
|
||||
export class NavigationMenuItemEntity
|
||||
extends SyncableEntity
|
||||
implements Required<NavigationMenuItemEntity>
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ManyToOne(() => UserWorkspaceEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'userWorkspaceId' })
|
||||
userWorkspace: Relation<UserWorkspaceEntity> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
userWorkspaceId: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
targetRecordId: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
targetObjectMetadataId: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
viewId: string | null;
|
||||
|
||||
@ManyToOne(() => ObjectMetadataEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'targetObjectMetadataId' })
|
||||
targetObjectMetadata: Relation<ObjectMetadataEntity> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
name: string | null;
|
||||
|
||||
@ManyToOne(() => NavigationMenuItemEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'folderId' })
|
||||
folder: Relation<NavigationMenuItemEntity> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
folderId: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
position: number;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { navigationMenuItemGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/navigation-menu-item/utils/navigation-menu-item-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class NavigationMenuItemGraphqlApiExceptionInterceptor
|
||||
implements NestInterceptor
|
||||
{
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next
|
||||
.handle()
|
||||
.pipe(catchError(navigationMenuItemGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum NavigationMenuItemExceptionCode {
|
||||
NAVIGATION_MENU_ITEM_NOT_FOUND = 'NAVIGATION_MENU_ITEM_NOT_FOUND',
|
||||
INVALID_NAVIGATION_MENU_ITEM_INPUT = 'INVALID_NAVIGATION_MENU_ITEM_INPUT',
|
||||
CIRCULAR_DEPENDENCY = 'CIRCULAR_DEPENDENCY',
|
||||
MAX_DEPTH_EXCEEDED = 'MAX_DEPTH_EXCEEDED',
|
||||
}
|
||||
|
||||
const getNavigationMenuItemExceptionUserFriendlyMessage = (
|
||||
code: NavigationMenuItemExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND:
|
||||
return msg`Navigation menu item not found.`;
|
||||
case NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT:
|
||||
return msg`Invalid navigation menu item input.`;
|
||||
case NavigationMenuItemExceptionCode.CIRCULAR_DEPENDENCY:
|
||||
return msg`Circular dependency detected in navigation menu item hierarchy.`;
|
||||
case NavigationMenuItemExceptionCode.MAX_DEPTH_EXCEEDED:
|
||||
return msg`Navigation menu item hierarchy exceeds maximum depth.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
};
|
||||
|
||||
export class NavigationMenuItemException extends CustomException<NavigationMenuItemExceptionCode> {
|
||||
constructor(
|
||||
message: string,
|
||||
code: NavigationMenuItemExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
||||
) {
|
||||
super(message, code, {
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage ??
|
||||
getNavigationMenuItemExceptionUserFriendlyMessage(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FlatNavigationMenuItemModule } from 'src/engine/metadata-modules/flat-navigation-menu-item/flat-navigation-menu-item.module';
|
||||
import { NavigationMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/navigation-menu-item/interceptors/navigation-menu-item-graphql-api-exception.interceptor';
|
||||
import { NavigationMenuItemResolver } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver';
|
||||
import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service';
|
||||
import { NavigationMenuItemAccessService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-access.service';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
ApplicationModule,
|
||||
FlatNavigationMenuItemModule,
|
||||
PermissionsModule,
|
||||
],
|
||||
providers: [
|
||||
NavigationMenuItemService,
|
||||
NavigationMenuItemAccessService,
|
||||
NavigationMenuItemResolver,
|
||||
NavigationMenuItemGraphqlApiExceptionInterceptor,
|
||||
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [NavigationMenuItemService],
|
||||
})
|
||||
export class NavigationMenuItemModule {}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthApiKey } from 'src/engine/decorators/auth/auth-api-key.decorator';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input';
|
||||
import { NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
import { UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
|
||||
import { NavigationMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/navigation-menu-item/interceptors/navigation-menu-item-graphql-api-exception.interceptor';
|
||||
import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service';
|
||||
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(
|
||||
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
|
||||
NavigationMenuItemGraphqlApiExceptionInterceptor,
|
||||
)
|
||||
@Resolver(() => NavigationMenuItemDTO)
|
||||
export class NavigationMenuItemResolver {
|
||||
constructor(
|
||||
private readonly navigationMenuItemService: NavigationMenuItemService,
|
||||
) {}
|
||||
|
||||
@Query(() => [NavigationMenuItemDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async navigationMenuItems(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
): Promise<NavigationMenuItemDTO[]> {
|
||||
return await this.navigationMenuItemService.findAll({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => NavigationMenuItemDTO, { nullable: true })
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async navigationMenuItem(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<NavigationMenuItemDTO | null> {
|
||||
return await this.navigationMenuItemService.findById({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => NavigationMenuItemDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async createNavigationMenuItem(
|
||||
@Args('input') input: CreateNavigationMenuItemInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@Context() context: { req: { application?: ApplicationEntity } },
|
||||
): Promise<NavigationMenuItemDTO> {
|
||||
return await this.navigationMenuItemService.create({
|
||||
input,
|
||||
workspaceId: workspace.id,
|
||||
authUserWorkspaceId: userWorkspaceId,
|
||||
authApiKeyId: apiKey?.id,
|
||||
authApplicationId: context.req.application?.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => NavigationMenuItemDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async updateNavigationMenuItem(
|
||||
@Args('input') input: UpdateOneNavigationMenuItemInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@Context() context: { req: { application?: ApplicationEntity } },
|
||||
): Promise<NavigationMenuItemDTO> {
|
||||
return await this.navigationMenuItemService.update({
|
||||
input: { ...input.update, id: input.id },
|
||||
workspaceId: workspace.id,
|
||||
authUserWorkspaceId: userWorkspaceId,
|
||||
authApiKeyId: apiKey?.id,
|
||||
authApplicationId: context.req.application?.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => NavigationMenuItemDTO)
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async deleteNavigationMenuItem(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@Context() context: { req: { application?: ApplicationEntity } },
|
||||
): Promise<NavigationMenuItemDTO> {
|
||||
return await this.navigationMenuItemService.delete({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
authUserWorkspaceId: userWorkspaceId,
|
||||
authApiKeyId: apiKey?.id,
|
||||
authApplicationId: context.req.application?.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-create-navigation-menu-item-input-to-flat-navigation-menu-item-to-create.util';
|
||||
import { fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-delete-navigation-menu-item-input-to-flat-navigation-menu-item-or-throw.util';
|
||||
import { fromFlatNavigationMenuItemToNavigationMenuItemDto } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-flat-navigation-menu-item-to-navigation-menu-item-dto.util';
|
||||
import { fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-update-navigation-menu-item-input-to-flat-navigation-menu-item-to-update-or-throw.util';
|
||||
import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input';
|
||||
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
import { type UpdateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
|
||||
import {
|
||||
NavigationMenuItemException,
|
||||
NavigationMenuItemExceptionCode,
|
||||
} from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||
import { NavigationMenuItemAccessService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-access.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class NavigationMenuItemService {
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly navigationMenuItemAccessService: NavigationMenuItemAccessService,
|
||||
) {}
|
||||
|
||||
async findAll({
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
userWorkspaceId?: string;
|
||||
}): Promise<NavigationMenuItemDTO[]> {
|
||||
const { flatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return Object.values(flatNavigationMenuItemMaps.byId)
|
||||
.filter(
|
||||
(item): item is NonNullable<typeof item> =>
|
||||
isDefined(item) &&
|
||||
(!isDefined(item.userWorkspaceId) ||
|
||||
item.userWorkspaceId === userWorkspaceId),
|
||||
)
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map(fromFlatNavigationMenuItemToNavigationMenuItemDto);
|
||||
}
|
||||
|
||||
async findById({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<NavigationMenuItemDTO | null> {
|
||||
const { flatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatNavigationMenuItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromFlatNavigationMenuItemToNavigationMenuItemDto(
|
||||
flatNavigationMenuItem,
|
||||
);
|
||||
}
|
||||
|
||||
async findByIdOrThrow({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<NavigationMenuItemDTO> {
|
||||
const navigationMenuItem = await this.findById({ id, workspaceId });
|
||||
|
||||
if (!isDefined(navigationMenuItem)) {
|
||||
throw new NavigationMenuItemException(
|
||||
'Navigation menu item not found',
|
||||
NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return navigationMenuItem;
|
||||
}
|
||||
|
||||
async create({
|
||||
input,
|
||||
workspaceId,
|
||||
authUserWorkspaceId,
|
||||
authApiKeyId,
|
||||
authApplicationId,
|
||||
}: {
|
||||
input: CreateNavigationMenuItemInput;
|
||||
workspaceId: string;
|
||||
authUserWorkspaceId?: string;
|
||||
authApiKeyId?: string;
|
||||
authApplicationId?: string;
|
||||
}): Promise<NavigationMenuItemDTO> {
|
||||
await this.navigationMenuItemAccessService.canUserCreateNavigationMenuItem({
|
||||
userWorkspaceId: authUserWorkspaceId,
|
||||
workspaceId,
|
||||
apiKeyId: authApiKeyId,
|
||||
applicationId: authApplicationId,
|
||||
inputUserWorkspaceId: input.userWorkspaceId,
|
||||
});
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const normalizedInput: CreateNavigationMenuItemInput = {
|
||||
...input,
|
||||
userWorkspaceId:
|
||||
isDefined(input.userWorkspaceId) && isDefined(authUserWorkspaceId)
|
||||
? authUserWorkspaceId
|
||||
: input.userWorkspaceId,
|
||||
};
|
||||
|
||||
const flatNavigationMenuItemToCreate =
|
||||
fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate({
|
||||
createNavigationMenuItemInput: normalizedInput,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
navigationMenuItem: {
|
||||
flatEntityToCreate: [flatNavigationMenuItemToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating navigation menu item',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatNavigationMenuItemMaps: recomputedFlatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatNavigationMenuItemToNavigationMenuItemDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatNavigationMenuItemToCreate.id,
|
||||
flatEntityMaps: recomputedFlatNavigationMenuItemMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async update({
|
||||
input,
|
||||
workspaceId,
|
||||
authUserWorkspaceId,
|
||||
authApiKeyId,
|
||||
authApplicationId,
|
||||
}: {
|
||||
input: UpdateNavigationMenuItemInput & { id: string };
|
||||
workspaceId: string;
|
||||
authUserWorkspaceId?: string;
|
||||
authApiKeyId?: string;
|
||||
authApplicationId?: string;
|
||||
}): Promise<NavigationMenuItemDTO> {
|
||||
const { flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: input.id,
|
||||
flatEntityMaps: existingFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (isDefined(existingNavigationMenuItem)) {
|
||||
await this.navigationMenuItemAccessService.canUserUpdateNavigationMenuItem(
|
||||
{
|
||||
userWorkspaceId: authUserWorkspaceId,
|
||||
workspaceId,
|
||||
apiKeyId: authApiKeyId,
|
||||
applicationId: authApplicationId,
|
||||
existingUserWorkspaceId: existingNavigationMenuItem.userWorkspaceId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const flatNavigationMenuItemToUpdate =
|
||||
fromUpdateNavigationMenuItemInputToFlatNavigationMenuItemToUpdateOrThrow({
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
updateNavigationMenuItemInput: input,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
navigationMenuItem: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatNavigationMenuItemToUpdate],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating navigation menu item',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatNavigationMenuItemMaps: recomputedFlatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatNavigationMenuItemToNavigationMenuItemDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: input.id,
|
||||
flatEntityMaps: recomputedFlatNavigationMenuItemMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async delete({
|
||||
id,
|
||||
workspaceId,
|
||||
authUserWorkspaceId,
|
||||
authApiKeyId,
|
||||
authApplicationId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
authUserWorkspaceId?: string;
|
||||
authApiKeyId?: string;
|
||||
authApplicationId?: string;
|
||||
}): Promise<NavigationMenuItemDTO> {
|
||||
const { flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatNavigationMenuItemMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatNavigationMenuItemToDelete =
|
||||
fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow({
|
||||
flatNavigationMenuItemMaps: existingFlatNavigationMenuItemMaps,
|
||||
navigationMenuItemId: id,
|
||||
});
|
||||
|
||||
await this.navigationMenuItemAccessService.canUserDeleteNavigationMenuItem({
|
||||
userWorkspaceId: authUserWorkspaceId,
|
||||
workspaceId,
|
||||
apiKeyId: authApiKeyId,
|
||||
applicationId: authApplicationId,
|
||||
existingUserWorkspaceId: flatNavigationMenuItemToDelete.userWorkspaceId,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
navigationMenuItem: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [flatNavigationMenuItemToDelete],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting navigation menu item',
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatNavigationMenuItemToNavigationMenuItemDto(
|
||||
flatNavigationMenuItemToDelete,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
NavigationMenuItemException,
|
||||
NavigationMenuItemExceptionCode,
|
||||
} from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
|
||||
@Injectable()
|
||||
export class NavigationMenuItemAccessService {
|
||||
constructor(private readonly permissionsService: PermissionsService) {}
|
||||
|
||||
async canUserCreateNavigationMenuItem({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
apiKeyId,
|
||||
applicationId,
|
||||
inputUserWorkspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string | undefined;
|
||||
workspaceId: string;
|
||||
apiKeyId: string | undefined;
|
||||
applicationId: string | undefined;
|
||||
inputUserWorkspaceId: string | undefined;
|
||||
}): Promise<boolean> {
|
||||
if (!isDefined(inputUserWorkspaceId)) {
|
||||
const hasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
setting: PermissionFlagType.LAYOUTS,
|
||||
apiKeyId,
|
||||
applicationId,
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage: msg`You do not have permission to create workspace-level navigation menu items. Please contact your workspace administrator for access.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isDefined(userWorkspaceId)) {
|
||||
throw new NavigationMenuItemException(
|
||||
'User-level navigation menu items can only be created by authenticated users',
|
||||
NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async canUserUpdateNavigationMenuItem({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
apiKeyId,
|
||||
applicationId,
|
||||
existingUserWorkspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string | undefined;
|
||||
workspaceId: string;
|
||||
apiKeyId: string | undefined;
|
||||
applicationId: string | undefined;
|
||||
existingUserWorkspaceId: string | null | undefined;
|
||||
}): Promise<boolean> {
|
||||
if (!isDefined(existingUserWorkspaceId)) {
|
||||
const hasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
setting: PermissionFlagType.LAYOUTS,
|
||||
apiKeyId,
|
||||
applicationId,
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage: msg`You do not have permission to update workspace-level navigation menu items. Please contact your workspace administrator for access.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isDefined(userWorkspaceId)) {
|
||||
throw new NavigationMenuItemException(
|
||||
'User-level navigation menu items can only be updated by authenticated users',
|
||||
NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUserWorkspaceId !== userWorkspaceId) {
|
||||
throw new NavigationMenuItemException(
|
||||
'You can only update your own navigation menu items',
|
||||
NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async canUserDeleteNavigationMenuItem({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
apiKeyId,
|
||||
applicationId,
|
||||
existingUserWorkspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string | undefined;
|
||||
workspaceId: string;
|
||||
apiKeyId: string | undefined;
|
||||
applicationId: string | undefined;
|
||||
existingUserWorkspaceId: string | null | undefined;
|
||||
}): Promise<boolean> {
|
||||
if (!isDefined(existingUserWorkspaceId)) {
|
||||
const hasPermission =
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
setting: PermissionFlagType.LAYOUTS,
|
||||
apiKeyId,
|
||||
applicationId,
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage: msg`You do not have permission to delete workspace-level navigation menu items. Please contact your workspace administrator for access.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isDefined(userWorkspaceId)) {
|
||||
throw new NavigationMenuItemException(
|
||||
'User-level navigation menu items can only be deleted by authenticated users',
|
||||
NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUserWorkspaceId !== userWorkspaceId) {
|
||||
throw new NavigationMenuItemException(
|
||||
'You can only delete your own navigation menu items',
|
||||
NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
NavigationMenuItemException,
|
||||
NavigationMenuItemExceptionCode,
|
||||
} from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||
|
||||
export const navigationMenuItemGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof NavigationMenuItemException) {
|
||||
switch (error.code) {
|
||||
case NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT:
|
||||
case NavigationMenuItemExceptionCode.CIRCULAR_DEPENDENCY:
|
||||
case NavigationMenuItemExceptionCode.MAX_DEPTH_EXCEEDED:
|
||||
throw new UserInputError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
@@ -36,6 +36,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
|
||||
flatAgentMaps: 'flat-maps:agent',
|
||||
flatSkillMaps: 'flat-maps:skill',
|
||||
flatCommandMenuItemMaps: 'flat-maps:command-menu-item',
|
||||
flatNavigationMenuItemMaps: 'flat-maps:navigation-menu-item',
|
||||
flatRoleTargetByAgentIdMaps: 'flat-maps:flatRoleTargetByAgentId',
|
||||
flatPageLayoutMaps: 'flat-maps:page-layout',
|
||||
flatPageLayoutWidgetMaps: 'flat-maps:page-layout-widget',
|
||||
|
||||
@@ -21,6 +21,7 @@ import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src
|
||||
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
|
||||
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
|
||||
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
|
||||
import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service';
|
||||
import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service';
|
||||
import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service';
|
||||
import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service';
|
||||
@@ -58,6 +59,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
private readonly workspaceMigrationAgentActionsBuilderService: WorkspaceMigrationAgentActionsBuilderService,
|
||||
private readonly workspaceMigrationSkillActionsBuilderService: WorkspaceMigrationSkillActionsBuilderService,
|
||||
private readonly workspaceMigrationCommandMenuItemActionsBuilderService: WorkspaceMigrationCommandMenuItemActionsBuilderService,
|
||||
private readonly workspaceMigrationNavigationMenuItemActionsBuilderService: WorkspaceMigrationNavigationMenuItemActionsBuilderService,
|
||||
private readonly workspaceMigrationPageLayoutActionsBuilderService: WorkspaceMigrationPageLayoutActionsBuilderService,
|
||||
private readonly workspaceMigrationPageLayoutWidgetActionsBuilderService: WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
|
||||
private readonly workspaceMigrationPageLayoutTabActionsBuilderService: WorkspaceMigrationPageLayoutTabActionsBuilderService,
|
||||
@@ -161,6 +163,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
flatAgentMaps,
|
||||
flatSkillMaps,
|
||||
flatCommandMenuItemMaps,
|
||||
flatNavigationMenuItemMaps,
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
@@ -859,6 +862,46 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatNavigationMenuItemMaps)) {
|
||||
const {
|
||||
from: fromFlatNavigationMenuItemMaps,
|
||||
to: toFlatNavigationMenuItemMaps,
|
||||
} = flatNavigationMenuItemMaps;
|
||||
|
||||
const navigationMenuItemResult =
|
||||
await this.workspaceMigrationNavigationMenuItemActionsBuilderService.validateAndBuild(
|
||||
{
|
||||
additionalCacheDataMaps,
|
||||
from: fromFlatNavigationMenuItemMaps,
|
||||
to: toFlatNavigationMenuItemMaps,
|
||||
buildOptions,
|
||||
dependencyOptimisticFlatEntityMaps: {
|
||||
flatObjectMetadataMaps:
|
||||
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
|
||||
flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps,
|
||||
},
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
|
||||
{
|
||||
allFlatEntityMaps: optimisticAllFlatEntityMaps,
|
||||
flatEntityMapsAndRelatedFlatEntityMaps:
|
||||
navigationMenuItemResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (navigationMenuItemResult.status === 'fail') {
|
||||
orchestratorFailureReport.navigationMenuItem.push(
|
||||
...navigationMenuItemResult.errors,
|
||||
);
|
||||
} else {
|
||||
orchestratorActionsReport.navigationMenuItem =
|
||||
navigationMenuItemResult.actions;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatPageLayoutMaps)) {
|
||||
const { from: fromFlatPageLayoutMaps, to: toFlatPageLayoutMaps } =
|
||||
flatPageLayoutMaps;
|
||||
@@ -1118,6 +1161,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
...aggregatedOrchestratorActionsReport.commandMenuItem.update,
|
||||
///
|
||||
|
||||
// Navigation Menu Items
|
||||
...aggregatedOrchestratorActionsReport.navigationMenuItem.delete,
|
||||
...aggregatedOrchestratorActionsReport.navigationMenuItem.create,
|
||||
...aggregatedOrchestratorActionsReport.navigationMenuItem.update,
|
||||
///
|
||||
|
||||
// Page layouts
|
||||
...aggregatedOrchestratorActionsReport.pageLayout.delete,
|
||||
...aggregatedOrchestratorActionsReport.pageLayout.create,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { type BaseCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type';
|
||||
import { type BaseDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type';
|
||||
import { type BaseUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type';
|
||||
|
||||
export type CreateNavigationMenuItemAction =
|
||||
BaseCreateWorkspaceMigrationAction<'navigationMenuItem'>;
|
||||
|
||||
export type UpdateNavigationMenuItemAction =
|
||||
BaseUpdateWorkspaceMigrationAction<'navigationMenuItem'>;
|
||||
|
||||
export type DeleteNavigationMenuItemAction =
|
||||
BaseDeleteWorkspaceMigrationAction<'navigationMenuItem'>;
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
|
||||
import { UpdateNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
|
||||
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
|
||||
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
|
||||
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
|
||||
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-result.type';
|
||||
import { FlatNavigationMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationNavigationMenuItemActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatNavigationMenuItemValidatorService: FlatNavigationMenuItemValidatorService,
|
||||
) {
|
||||
super(ALL_METADATA_NAME.navigationMenuItem);
|
||||
}
|
||||
|
||||
protected validateFlatEntityCreation(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.navigationMenuItem>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem,
|
||||
'create'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatNavigationMenuItemValidatorService.validateFlatNavigationMenuItemCreation(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatNavigationMenuItemToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create',
|
||||
metadataName: 'navigationMenuItem',
|
||||
flatEntity: flatNavigationMenuItemToValidate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityDeletion(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.navigationMenuItem>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem,
|
||||
'delete'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatNavigationMenuItemValidatorService.validateFlatNavigationMenuItemDeletion(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatNavigationMenuItemToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete',
|
||||
metadataName: 'navigationMenuItem',
|
||||
universalIdentifier:
|
||||
flatNavigationMenuItemToValidate.universalIdentifier,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityUpdate(
|
||||
args: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem
|
||||
>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem,
|
||||
'update'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatNavigationMenuItemValidatorService.validateFlatNavigationMenuItemUpdate(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityId, flatEntityUpdates } = args;
|
||||
|
||||
const updateNavigationMenuItemAction: UpdateNavigationMenuItemAction = {
|
||||
type: 'update',
|
||||
metadataName: 'navigationMenuItem',
|
||||
entityId: flatEntityId,
|
||||
updates: flatEntityUpdates,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: updateNavigationMenuItemAction,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { NavigationMenuItemExceptionCode } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.exception';
|
||||
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/find-flat-entity-property-update.util';
|
||||
import { validateFlatEntityCircularDependency } from 'src/engine/workspace-manager/workspace-migration/utils/validate-flat-entity-circular-dependency.util';
|
||||
import {
|
||||
type FailedFlatEntityValidation,
|
||||
type FlatEntityValidationError,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
|
||||
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
const NAVIGATION_MENU_ITEM_MAX_DEPTH = 2;
|
||||
|
||||
@Injectable()
|
||||
export class FlatNavigationMenuItemValidatorService {
|
||||
private validateNavigationMenuItemType({
|
||||
hasTargetRecordId,
|
||||
hasTargetObjectMetadataId,
|
||||
hasViewId,
|
||||
name,
|
||||
isUpdate = false,
|
||||
}: {
|
||||
hasTargetRecordId: boolean;
|
||||
hasTargetObjectMetadataId: boolean;
|
||||
hasViewId: boolean;
|
||||
name: string | null | undefined;
|
||||
isUpdate?: boolean;
|
||||
}): FlatEntityValidationError<NavigationMenuItemExceptionCode>[] {
|
||||
const errors: FlatEntityValidationError<NavigationMenuItemExceptionCode>[] =
|
||||
[];
|
||||
|
||||
if (hasTargetObjectMetadataId && !hasTargetRecordId) {
|
||||
errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`targetRecordId is required when targetObjectMetadataId is provided`,
|
||||
userFriendlyMessage: msg`targetRecordId is required when targetObjectMetadataId is provided`,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasTargetRecordId && !hasTargetObjectMetadataId) {
|
||||
errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`targetObjectMetadataId is required when targetRecordId is provided`,
|
||||
userFriendlyMessage: msg`targetObjectMetadataId is required when targetRecordId is provided`,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasViewId && (hasTargetRecordId || hasTargetObjectMetadataId)) {
|
||||
errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`viewId cannot be provided together with targetRecordId or targetObjectMetadataId`,
|
||||
userFriendlyMessage: msg`viewId cannot be provided together with targetRecordId or targetObjectMetadataId`,
|
||||
});
|
||||
}
|
||||
|
||||
const isFolder =
|
||||
!hasTargetRecordId && !hasTargetObjectMetadataId && !hasViewId;
|
||||
const isViewLink = hasViewId;
|
||||
const isRecordLink = hasTargetRecordId && hasTargetObjectMetadataId;
|
||||
const typeCount =
|
||||
(isFolder ? 1 : 0) + (isViewLink ? 1 : 0) + (isRecordLink ? 1 : 0);
|
||||
|
||||
if (typeCount === 0) {
|
||||
errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)`,
|
||||
userFriendlyMessage: msg`Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)`,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeCount > 1) {
|
||||
errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Navigation menu item cannot be multiple types simultaneously`,
|
||||
userFriendlyMessage: msg`Navigation menu item cannot be multiple types simultaneously`,
|
||||
});
|
||||
}
|
||||
|
||||
if (isFolder && (!isDefined(name) || name.trim() === '')) {
|
||||
errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: isUpdate
|
||||
? t`Folder name is required and cannot be empty`
|
||||
: t`Folder name is required when creating a folder`,
|
||||
userFriendlyMessage: isUpdate
|
||||
? msg`Folder name is required and cannot be empty`
|
||||
: msg`Folder name is required when creating a folder`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private getCircularDependencyValidationErrors({
|
||||
navigationMenuItemId,
|
||||
folderId,
|
||||
flatNavigationMenuItemMaps,
|
||||
}: {
|
||||
navigationMenuItemId: string;
|
||||
folderId: string;
|
||||
flatNavigationMenuItemMaps: FlatNavigationMenuItemMaps;
|
||||
}): FlatEntityValidationError<NavigationMenuItemExceptionCode>[] {
|
||||
const circularDependencyResult = validateFlatEntityCircularDependency({
|
||||
flatEntityId: navigationMenuItemId,
|
||||
flatEntityParentId: folderId,
|
||||
maxDepth: NAVIGATION_MENU_ITEM_MAX_DEPTH,
|
||||
parentIdKey: 'folderId',
|
||||
flatEntityMaps: flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (circularDependencyResult.status === 'success') {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (circularDependencyResult.reason) {
|
||||
case 'self_reference':
|
||||
return [
|
||||
{
|
||||
code: NavigationMenuItemExceptionCode.CIRCULAR_DEPENDENCY,
|
||||
message: t`Navigation menu item cannot be its own parent`,
|
||||
userFriendlyMessage: msg`Navigation menu item cannot be its own parent`,
|
||||
},
|
||||
];
|
||||
case 'circular_dependency':
|
||||
return [
|
||||
{
|
||||
code: NavigationMenuItemExceptionCode.CIRCULAR_DEPENDENCY,
|
||||
message: t`Circular dependency detected in navigation menu item hierarchy`,
|
||||
userFriendlyMessage: msg`Circular dependency detected in navigation menu item hierarchy`,
|
||||
},
|
||||
];
|
||||
case 'max_depth_exceeded':
|
||||
return [
|
||||
{
|
||||
code: NavigationMenuItemExceptionCode.MAX_DEPTH_EXCEEDED,
|
||||
message: t`Navigation menu item hierarchy exceeds maximum depth of ${NAVIGATION_MENU_ITEM_MAX_DEPTH}`,
|
||||
userFriendlyMessage: msg`Navigation menu item hierarchy exceeds maximum depth of ${NAVIGATION_MENU_ITEM_MAX_DEPTH}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public validateFlatNavigationMenuItemCreation({
|
||||
flatEntityToValidate: flatNavigationMenuItem,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
},
|
||||
remainingFlatEntityMapsToValidate,
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem
|
||||
>): FailedFlatEntityValidation<'navigationMenuItem', 'create'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatNavigationMenuItem.id,
|
||||
universalIdentifier: flatNavigationMenuItem.universalIdentifier,
|
||||
},
|
||||
metadataName: 'navigationMenuItem',
|
||||
type: 'create',
|
||||
});
|
||||
|
||||
if (
|
||||
isDefined(flatNavigationMenuItem.position) &&
|
||||
(!Number.isInteger(flatNavigationMenuItem.position) ||
|
||||
flatNavigationMenuItem.position < 0)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Position must be a non-negative integer`,
|
||||
userFriendlyMessage: msg`Position must be a non-negative integer`,
|
||||
});
|
||||
}
|
||||
|
||||
const typeValidationErrors = this.validateNavigationMenuItemType({
|
||||
hasTargetRecordId: isDefined(flatNavigationMenuItem.targetRecordId),
|
||||
hasTargetObjectMetadataId: isDefined(
|
||||
flatNavigationMenuItem.targetObjectMetadataId,
|
||||
),
|
||||
hasViewId: isDefined(flatNavigationMenuItem.viewId),
|
||||
name: flatNavigationMenuItem.name,
|
||||
isUpdate: false,
|
||||
});
|
||||
|
||||
validationResult.errors.push(...typeValidationErrors);
|
||||
|
||||
if (isDefined(flatNavigationMenuItem.folderId)) {
|
||||
const circularDependencyErrors =
|
||||
this.getCircularDependencyValidationErrors({
|
||||
navigationMenuItemId: flatNavigationMenuItem.id,
|
||||
folderId: flatNavigationMenuItem.folderId,
|
||||
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (circularDependencyErrors.length > 0) {
|
||||
validationResult.errors.push(...circularDependencyErrors);
|
||||
}
|
||||
|
||||
const referencedParentInOptimistic = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatNavigationMenuItem.folderId,
|
||||
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
const referencedParentInRemaining = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatNavigationMenuItem.folderId,
|
||||
flatEntityMaps: remainingFlatEntityMapsToValidate,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(referencedParentInOptimistic) &&
|
||||
!isDefined(referencedParentInRemaining)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
|
||||
message: t`Parent navigation menu item not found`,
|
||||
userFriendlyMessage: msg`Parent navigation menu item not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatNavigationMenuItemDeletion({
|
||||
flatEntityToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
},
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem
|
||||
>): FailedFlatEntityValidation<'navigationMenuItem', 'delete'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatEntityToValidate.id,
|
||||
universalIdentifier: flatEntityToValidate.universalIdentifier,
|
||||
},
|
||||
metadataName: 'navigationMenuItem',
|
||||
type: 'delete',
|
||||
});
|
||||
|
||||
const existingNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatEntityToValidate.id,
|
||||
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingNavigationMenuItem)) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
|
||||
message: t`Navigation menu item not found`,
|
||||
userFriendlyMessage: msg`Navigation menu item not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatNavigationMenuItemUpdate({
|
||||
flatEntityId,
|
||||
flatEntityUpdates,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
},
|
||||
}: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.navigationMenuItem
|
||||
>): FailedFlatEntityValidation<'navigationMenuItem', 'update'> {
|
||||
const fromFlatNavigationMenuItem = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId,
|
||||
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatEntityId,
|
||||
universalIdentifier: fromFlatNavigationMenuItem?.universalIdentifier,
|
||||
},
|
||||
metadataName: 'navigationMenuItem',
|
||||
type: 'update',
|
||||
});
|
||||
|
||||
if (!isDefined(fromFlatNavigationMenuItem)) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
|
||||
message: t`Navigation menu item not found`,
|
||||
userFriendlyMessage: msg`Navigation menu item not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const positionUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'position',
|
||||
});
|
||||
|
||||
if (
|
||||
isDefined(positionUpdate) &&
|
||||
(!Number.isInteger(positionUpdate.to) || positionUpdate.to < 0)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.INVALID_NAVIGATION_MENU_ITEM_INPUT,
|
||||
message: t`Position must be a non-negative integer`,
|
||||
userFriendlyMessage: msg`Position must be a non-negative integer`,
|
||||
});
|
||||
}
|
||||
|
||||
const toFlatNavigationMenuItem = {
|
||||
...fromFlatNavigationMenuItem,
|
||||
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
|
||||
updates: flatEntityUpdates,
|
||||
}),
|
||||
};
|
||||
|
||||
const nameUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'name',
|
||||
});
|
||||
|
||||
const typeValidationErrors = this.validateNavigationMenuItemType({
|
||||
hasTargetRecordId: isDefined(toFlatNavigationMenuItem.targetRecordId),
|
||||
hasTargetObjectMetadataId: isDefined(
|
||||
toFlatNavigationMenuItem.targetObjectMetadataId,
|
||||
),
|
||||
hasViewId: isDefined(toFlatNavigationMenuItem.viewId),
|
||||
name: isDefined(nameUpdate)
|
||||
? nameUpdate.to
|
||||
: toFlatNavigationMenuItem.name,
|
||||
isUpdate: true,
|
||||
});
|
||||
|
||||
validationResult.errors.push(...typeValidationErrors);
|
||||
|
||||
const folderIdUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'folderId',
|
||||
});
|
||||
|
||||
if (!isDefined(folderIdUpdate)) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const newFolderId = folderIdUpdate.to;
|
||||
|
||||
if (!isDefined(newFolderId)) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const circularDependencyErrors = this.getCircularDependencyValidationErrors(
|
||||
{
|
||||
navigationMenuItemId: flatEntityId,
|
||||
folderId: newFolderId,
|
||||
flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (circularDependencyErrors.length > 0) {
|
||||
validationResult.errors.push(...circularDependencyErrors);
|
||||
}
|
||||
|
||||
const referencedParentNavigationMenuItem =
|
||||
findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: newFolderId,
|
||||
flatEntityMaps: optimisticFlatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(referencedParentNavigationMenuItem)) {
|
||||
validationResult.errors.push({
|
||||
code: NavigationMenuItemExceptionCode.NAVIGATION_MENU_ITEM_NOT_FOUND,
|
||||
message: t`Parent navigation menu item not found`,
|
||||
userFriendlyMessage: msg`Parent navigation menu item not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
|
||||
import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service';
|
||||
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-agent-validator.service';
|
||||
import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-command-menu-item-validator.service';
|
||||
import { FlatCronTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-cron-trigger-validator.service';
|
||||
import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-database-event-trigger-validator.service';
|
||||
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-field-metadata-validator.service';
|
||||
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
|
||||
import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-index-metadata-validator.service';
|
||||
import { FlatNavigationMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service';
|
||||
import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service';
|
||||
import { FlatPageLayoutTabValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service';
|
||||
import { FlatPageLayoutValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-validator.service';
|
||||
@@ -18,9 +21,7 @@ import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/w
|
||||
import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-group-validator.service';
|
||||
import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-validator.service';
|
||||
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-serverless-function-validator.service';
|
||||
import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-command-menu-item-validator.service';
|
||||
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-skill-validator.service';
|
||||
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
|
||||
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service';
|
||||
import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-group-validator.service';
|
||||
import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service';
|
||||
@@ -49,6 +50,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatAgentValidatorService,
|
||||
FlatSkillValidatorService,
|
||||
FlatCommandMenuItemValidatorService,
|
||||
FlatNavigationMenuItemValidatorService,
|
||||
FlatPageLayoutValidatorService,
|
||||
FlatPageLayoutWidgetValidatorService,
|
||||
FlatPageLayoutTabValidatorService,
|
||||
@@ -75,6 +77,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatAgentValidatorService,
|
||||
FlatSkillValidatorService,
|
||||
FlatCommandMenuItemValidatorService,
|
||||
FlatNavigationMenuItemValidatorService,
|
||||
FlatPageLayoutValidatorService,
|
||||
FlatPageLayoutWidgetValidatorService,
|
||||
FlatPageLayoutTabValidatorService,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
|
||||
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
|
||||
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
|
||||
import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service';
|
||||
import { WorkspaceMigrationCronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/cron-trigger/workspace-migration-cron-trigger-action-builder.service';
|
||||
import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/database-event-trigger/workspace-migration-database-event-trigger-actions-builder.service';
|
||||
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
|
||||
@@ -48,6 +49,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationAgentActionsBuilderService,
|
||||
WorkspaceMigrationSkillActionsBuilderService,
|
||||
WorkspaceMigrationCommandMenuItemActionsBuilderService,
|
||||
WorkspaceMigrationNavigationMenuItemActionsBuilderService,
|
||||
WorkspaceMigrationPageLayoutActionsBuilderService,
|
||||
WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
|
||||
WorkspaceMigrationPageLayoutTabActionsBuilderService,
|
||||
@@ -73,6 +75,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationAgentActionsBuilderService,
|
||||
WorkspaceMigrationSkillActionsBuilderService,
|
||||
WorkspaceMigrationCommandMenuItemActionsBuilderService,
|
||||
WorkspaceMigrationNavigationMenuItemActionsBuilderService,
|
||||
WorkspaceMigrationPageLayoutActionsBuilderService,
|
||||
WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
|
||||
WorkspaceMigrationPageLayoutTabActionsBuilderService,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { CreateNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'create',
|
||||
'navigationMenuItem',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<CreateNavigationMenuItemAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { flatEntity } = action;
|
||||
|
||||
const navigationMenuItemRepository =
|
||||
queryRunner.manager.getRepository<NavigationMenuItemEntity>(
|
||||
NavigationMenuItemEntity,
|
||||
);
|
||||
|
||||
await navigationMenuItemRepository.insert({
|
||||
...flatEntity,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<CreateNavigationMenuItemAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { DeleteNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteNavigationMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'delete',
|
||||
'navigationMenuItem',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<DeleteNavigationMenuItemAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId, allFlatEntityMaps } = context;
|
||||
const { universalIdentifier } = action;
|
||||
|
||||
const flatNavigationMenuItem = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: allFlatEntityMaps.flatNavigationMenuItemMaps,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
const navigationMenuItemRepository =
|
||||
queryRunner.manager.getRepository<NavigationMenuItemEntity>(
|
||||
NavigationMenuItemEntity,
|
||||
);
|
||||
|
||||
await navigationMenuItemRepository.delete({
|
||||
id: flatNavigationMenuItem.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<DeleteNavigationMenuItemAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { UpdateNavigationMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateNavigationMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'update',
|
||||
'navigationMenuItem',
|
||||
) {
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UpdateNavigationMenuItemAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { entityId, updates } = action;
|
||||
|
||||
const navigationMenuItemRepository =
|
||||
queryRunner.manager.getRepository<NavigationMenuItemEntity>(
|
||||
NavigationMenuItemEntity,
|
||||
);
|
||||
|
||||
await navigationMenuItemRepository.update(
|
||||
{ id: entityId, workspaceId },
|
||||
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
|
||||
updates,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<UpdateNavigationMenuItemAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/wo
|
||||
import { CreateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service';
|
||||
import { DeleteCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/delete-command-menu-item-action-handler.service';
|
||||
import { UpdateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/update-command-menu-item-action-handler.service';
|
||||
import { CreateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service';
|
||||
import { DeleteNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/delete-navigation-menu-item-action-handler.service';
|
||||
import { UpdateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/update-navigation-menu-item-action-handler.service';
|
||||
import { CreateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/create-cron-trigger-action-handler.service';
|
||||
import { DeleteCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/delete-cron-trigger-action-handler.service';
|
||||
import { UpdateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/update-cron-trigger-action-handler.service';
|
||||
@@ -142,6 +145,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
|
||||
UpdateCommandMenuItemActionHandlerService,
|
||||
DeleteCommandMenuItemActionHandlerService,
|
||||
|
||||
CreateNavigationMenuItemActionHandlerService,
|
||||
UpdateNavigationMenuItemActionHandlerService,
|
||||
DeleteNavigationMenuItemActionHandlerService,
|
||||
|
||||
CreatePageLayoutActionHandlerService,
|
||||
UpdatePageLayoutActionHandlerService,
|
||||
DeletePageLayoutActionHandlerService,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
|
||||
import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
|
||||
import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/add-flat-navigation-menu-item-to-maps-and-update-index.util';
|
||||
|
||||
type CreateAction<TMetadataName extends AllMetadataName> =
|
||||
AllFlatEntityTypesByMetadataName[TMetadataName]['actions']['create'];
|
||||
@@ -79,6 +80,15 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = <
|
||||
|
||||
return allFlatEntityMaps;
|
||||
}
|
||||
case 'navigationMenuItem': {
|
||||
addFlatNavigationMenuItemToMapsAndUpdateIndex({
|
||||
flatNavigationMenuItem: action.flatEntity,
|
||||
flatNavigationMenuItemMaps:
|
||||
allFlatEntityMaps.flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
return allFlatEntityMaps;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(action);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity
|
||||
import { deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { deleteFlatNavigationMenuItemFromMapsAndIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/delete-flat-navigation-menu-item-from-maps-and-index.util';
|
||||
|
||||
type DeleteAction<TMetadataName extends AllMetadataName> =
|
||||
AllFlatEntityTypesByMetadataName[TMetadataName]['actions']['delete'];
|
||||
@@ -64,6 +65,21 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = <
|
||||
|
||||
return allFlatEntityMaps;
|
||||
}
|
||||
case 'navigationMenuItem': {
|
||||
const flatNavigationMenuItemToDelete =
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
universalIdentifier: action.universalIdentifier,
|
||||
flatEntityMaps: allFlatEntityMaps.flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
deleteFlatNavigationMenuItemFromMapsAndIndex({
|
||||
flatNavigationMenuItem: flatNavigationMenuItemToDelete,
|
||||
flatNavigationMenuItemMaps:
|
||||
allFlatEntityMaps.flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
return allFlatEntityMaps;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(action);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } f
|
||||
import { deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { replaceFlatNavigationMenuItemInMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/replace-flat-navigation-menu-item-in-maps-and-update-index.util';
|
||||
import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
@@ -92,6 +93,27 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = <
|
||||
|
||||
return allFlatEntityMaps;
|
||||
}
|
||||
case 'navigationMenuItem': {
|
||||
const fromFlatNavigationMenuItem =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: action.entityId,
|
||||
flatEntityMaps: allFlatEntityMaps.flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
const toFlatNavigationMenuItem = {
|
||||
...fromFlatNavigationMenuItem,
|
||||
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
|
||||
};
|
||||
|
||||
replaceFlatNavigationMenuItemInMapsAndUpdateIndex({
|
||||
fromFlatNavigationMenuItem,
|
||||
toFlatNavigationMenuItem,
|
||||
flatNavigationMenuItemMaps:
|
||||
allFlatEntityMaps.flatNavigationMenuItemMaps,
|
||||
});
|
||||
|
||||
return allFlatEntityMaps;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(action);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with empty targetObjectMetadataId 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with empty targetRecordId 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with invalid folderId (not a UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with invalid userWorkspaceId (not a UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with invalid targetObjectMetadataId (not a UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with invalid targetRecordId (not a UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with missing targetObjectMetadataId 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"navigationMenuItem": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||
"message": "targetObjectMetadataId is required when targetRecordId is provided",
|
||||
"userFriendlyMessage": "targetObjectMetadataId is required when targetRecordId is provided",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||
"message": "Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)",
|
||||
"userFriendlyMessage": "Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "navigationMenuItem",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 navigationMenuItem",
|
||||
"summary": {
|
||||
"navigationMenuItem": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating navigation menu item",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with missing targetRecordId 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"navigationMenuItem": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||
"message": "targetRecordId is required when targetObjectMetadataId is provided",
|
||||
"userFriendlyMessage": "targetRecordId is required when targetObjectMetadataId is provided",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||
"message": "Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)",
|
||||
"userFriendlyMessage": "Navigation menu item must be either a folder (with name), a view link (with viewId), or a record link (with targetRecordId and targetObjectMetadataId)",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "navigationMenuItem",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 navigationMenuItem",
|
||||
"summary": {
|
||||
"navigationMenuItem": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating navigation menu item",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem creation should fail when creating with negative position 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"navigationMenuItem": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||
"message": "Position must be a non-negative integer",
|
||||
"userFriendlyMessage": "Position must be a non-negative integer",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "navigationMenuItem",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 navigationMenuItem",
|
||||
"summary": {
|
||||
"navigationMenuItem": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating navigation menu item",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,57 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`NavigationMenuItem deletion should fail when deleting a non-existent navigation menu item 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "NAVIGATION_MENU_ITEM_NOT_FOUND",
|
||||
"userFriendlyMessage": "Navigation menu item not found.",
|
||||
},
|
||||
"message": "Navigation menu item not found",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem deletion should fail when deleting with empty id 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem deletion should fail when deleting with invalid id (not a UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem deletion should fail when deleting with missing id 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Variable "$id" of required type "UUID!" was not provided.",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,73 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Navigation Menu Item update should fail with circular dependency when folderId equals id (self-reference) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"navigationMenuItem": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "CIRCULAR_DEPENDENCY",
|
||||
"message": "Navigation menu item cannot be its own parent",
|
||||
"userFriendlyMessage": "Navigation menu item cannot be its own parent",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "navigationMenuItem",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 navigationMenuItem",
|
||||
"summary": {
|
||||
"navigationMenuItem": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while updating navigation menu item",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Navigation Menu Item update should fail with circular dependency when update creates a circular dependency chain 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"navigationMenuItem": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "CIRCULAR_DEPENDENCY",
|
||||
"message": "Circular dependency detected in navigation menu item hierarchy",
|
||||
"userFriendlyMessage": "Circular dependency detected in navigation menu item hierarchy",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "navigationMenuItem",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 navigationMenuItem",
|
||||
"summary": {
|
||||
"navigationMenuItem": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while updating navigation menu item",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,109 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`NavigationMenuItem update should fail when updating a non-existent navigation menu item 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "NAVIGATION_MENU_ITEM_NOT_FOUND",
|
||||
"userFriendlyMessage": "Navigation menu item not found.",
|
||||
},
|
||||
"message": "Navigation menu item not found",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem update should fail when updating with empty id 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem update should fail when updating with invalid folderId (not a UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem update should fail when updating with invalid id (not a UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "not-a-valid-uuid",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem update should fail when updating with missing id 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "GRAPHQL_VALIDATION_FAILED",
|
||||
"http": {
|
||||
"status": 400,
|
||||
},
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
"value": "",
|
||||
},
|
||||
"message": "Invalid UUID",
|
||||
"name": "ValidationError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`NavigationMenuItem update should fail when updating with negative position 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"navigationMenuItem": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_NAVIGATION_MENU_ITEM_INPUT",
|
||||
"message": "Position must be a non-negative integer",
|
||||
"userFriendlyMessage": "Position must be a non-negative integer",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "navigationMenuItem",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 navigationMenuItem",
|
||||
"summary": {
|
||||
"navigationMenuItem": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while updating navigation menu item",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input';
|
||||
|
||||
type TestContext = {
|
||||
input: CreateNavigationMenuItemInput;
|
||||
};
|
||||
|
||||
const failingNavigationMenuItemCreationTestCases: EachTestingContext<TestContext>[] =
|
||||
[
|
||||
{
|
||||
title: 'when creating with missing targetRecordId',
|
||||
context: {
|
||||
input: {
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
} as CreateNavigationMenuItemInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with empty targetRecordId',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: '',
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with invalid targetRecordId (not a UUID)',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: 'not-a-valid-uuid',
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with missing targetObjectMetadataId',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: faker.string.uuid(),
|
||||
} as CreateNavigationMenuItemInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with empty targetObjectMetadataId',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: faker.string.uuid(),
|
||||
targetObjectMetadataId: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with invalid targetObjectMetadataId (not a UUID)',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: faker.string.uuid(),
|
||||
targetObjectMetadataId: 'not-a-valid-uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with invalid userWorkspaceId (not a UUID)',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: faker.string.uuid(),
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
userWorkspaceId: 'not-a-valid-uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with invalid folderId (not a UUID)',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: faker.string.uuid(),
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
folderId: 'not-a-valid-uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when creating with negative position',
|
||||
context: {
|
||||
input: {
|
||||
targetRecordId: faker.string.uuid(),
|
||||
targetObjectMetadataId: faker.string.uuid(),
|
||||
position: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('NavigationMenuItem creation should fail', () => {
|
||||
it.each(eachTestingContextFilter(failingNavigationMenuItemCreationTestCases))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const { errors } = await createNavigationMenuItem({
|
||||
expectToFail: true,
|
||||
input: context.input,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { type DeleteNavigationMenuItemFactoryInput } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item-query-factory.util';
|
||||
import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
type TestContext = {
|
||||
input: DeleteNavigationMenuItemFactoryInput;
|
||||
};
|
||||
|
||||
const failingNavigationMenuItemDeletionTestCases: EachTestingContext<TestContext>[] =
|
||||
[
|
||||
{
|
||||
title: 'when deleting a non-existent navigation menu item',
|
||||
context: {
|
||||
input: {
|
||||
id: faker.string.uuid(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when deleting with missing id',
|
||||
context: {
|
||||
input: {} as DeleteNavigationMenuItemFactoryInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when deleting with empty id',
|
||||
context: {
|
||||
input: {
|
||||
id: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when deleting with invalid id (not a UUID)',
|
||||
context: {
|
||||
input: {
|
||||
id: 'not-a-valid-uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('NavigationMenuItem deletion should fail', () => {
|
||||
it.each(eachTestingContextFilter(failingNavigationMenuItemDeletionTestCases))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const { errors } = await deleteNavigationMenuItem({
|
||||
expectToFail: true,
|
||||
input: context.input,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util';
|
||||
import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util';
|
||||
import { updateNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/update-navigation-menu-item.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
|
||||
describe('Navigation Menu Item update should fail with circular dependency', () => {
|
||||
let folderId: string;
|
||||
let parentFolderId: string;
|
||||
let childFolderId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data: folderData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'Standalone Folder',
|
||||
},
|
||||
});
|
||||
|
||||
folderId = folderData?.createNavigationMenuItem?.id;
|
||||
jestExpectToBeDefined(folderId);
|
||||
|
||||
const { data: parentFolderData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'Parent Folder',
|
||||
},
|
||||
});
|
||||
|
||||
parentFolderId = parentFolderData?.createNavigationMenuItem?.id;
|
||||
jestExpectToBeDefined(parentFolderId);
|
||||
|
||||
const { data: childFolderData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'Child Folder',
|
||||
folderId: parentFolderId,
|
||||
},
|
||||
});
|
||||
|
||||
childFolderId = childFolderData?.createNavigationMenuItem?.id;
|
||||
jestExpectToBeDefined(childFolderId);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (childFolderId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: childFolderId },
|
||||
});
|
||||
}
|
||||
|
||||
if (parentFolderId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: parentFolderId },
|
||||
});
|
||||
}
|
||||
|
||||
if (folderId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: folderId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('when folderId equals id (self-reference)', async () => {
|
||||
const { errors } = await updateNavigationMenuItem({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
id: folderId,
|
||||
update: {
|
||||
folderId: folderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
|
||||
it('when update creates a circular dependency chain', async () => {
|
||||
const { errors } = await updateNavigationMenuItem({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
id: parentFolderId,
|
||||
update: {
|
||||
folderId: childFolderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util';
|
||||
import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util';
|
||||
import { updateNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/update-navigation-menu-item.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
import { type UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
|
||||
|
||||
type TestContext = {
|
||||
input: (testSetup: TestSetup) => UpdateOneNavigationMenuItemInput;
|
||||
};
|
||||
|
||||
type TestSetup = {
|
||||
testNavigationMenuItemId: string;
|
||||
};
|
||||
|
||||
describe('NavigationMenuItem update should fail', () => {
|
||||
let testNavigationMenuItemId: string;
|
||||
let personObjectMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: { first: 1000 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
`,
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(objects);
|
||||
|
||||
const personObjectMetadata = objects.find(
|
||||
(object: { nameSingular: string }) => object.nameSingular === 'person',
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(personObjectMetadata);
|
||||
|
||||
personObjectMetadataId = personObjectMetadata.id;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const targetRecordId = faker.string.uuid();
|
||||
|
||||
const { data } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
position: 1,
|
||||
},
|
||||
});
|
||||
|
||||
testNavigationMenuItemId = data.createNavigationMenuItem.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (testNavigationMenuItemId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: testNavigationMenuItemId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const failingNavigationMenuItemUpdateTestCases: EachTestingContext<TestContext>[] =
|
||||
[
|
||||
{
|
||||
title: 'when updating with missing id',
|
||||
context: {
|
||||
input: () =>
|
||||
({
|
||||
id: '',
|
||||
update: {
|
||||
position: 10,
|
||||
},
|
||||
}) as UpdateOneNavigationMenuItemInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when updating with empty id',
|
||||
context: {
|
||||
input: () => ({
|
||||
id: '',
|
||||
update: {
|
||||
position: 10,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when updating with invalid id (not a UUID)',
|
||||
context: {
|
||||
input: () => ({
|
||||
id: 'not-a-valid-uuid',
|
||||
update: {
|
||||
position: 10,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when updating a non-existent navigation menu item',
|
||||
context: {
|
||||
input: () => ({
|
||||
id: faker.string.uuid(),
|
||||
update: {
|
||||
position: 10,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when updating with invalid folderId (not a UUID)',
|
||||
context: {
|
||||
input: (testSetup) => ({
|
||||
id: testSetup.testNavigationMenuItemId,
|
||||
update: {
|
||||
folderId: 'not-a-valid-uuid',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when updating with negative position',
|
||||
context: {
|
||||
input: (testSetup) => ({
|
||||
id: testSetup.testNavigationMenuItemId,
|
||||
update: {
|
||||
position: -1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(eachTestingContextFilter(failingNavigationMenuItemUpdateTestCases))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const testSetup: TestSetup = {
|
||||
testNavigationMenuItemId,
|
||||
};
|
||||
|
||||
const input = context.input(testSetup);
|
||||
|
||||
const { errors } = await updateNavigationMenuItem({
|
||||
expectToFail: true,
|
||||
input,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { getCurrentUser } from 'test/integration/graphql/utils/get-current-user.util';
|
||||
import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util';
|
||||
import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
|
||||
describe('NavigationMenuItem creation should succeed', () => {
|
||||
let createdNavigationMenuItemId: string;
|
||||
let companyObjectMetadataId: string;
|
||||
let personObjectMetadataId: string;
|
||||
let validUserWorkspaceId: string | null;
|
||||
let createdFolderId: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: { first: 1000 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
`,
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(objects);
|
||||
|
||||
const companyObjectMetadata = objects.find(
|
||||
(object: { nameSingular: string }) => object.nameSingular === 'company',
|
||||
);
|
||||
const personObjectMetadata = objects.find(
|
||||
(object: { nameSingular: string }) => object.nameSingular === 'person',
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(companyObjectMetadata);
|
||||
jestExpectToBeDefined(personObjectMetadata);
|
||||
|
||||
companyObjectMetadataId = companyObjectMetadata.id;
|
||||
personObjectMetadataId = personObjectMetadata.id;
|
||||
|
||||
const { data: currentUserData } = await getCurrentUser({
|
||||
accessToken: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(currentUserData?.currentUser?.currentUserWorkspace);
|
||||
|
||||
validUserWorkspaceId =
|
||||
currentUserData?.currentUser?.currentUserWorkspace?.id ?? null;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdNavigationMenuItemId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: createdNavigationMenuItemId },
|
||||
});
|
||||
createdNavigationMenuItemId = undefined as unknown as string;
|
||||
}
|
||||
if (createdFolderId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: createdFolderId },
|
||||
});
|
||||
createdFolderId = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should create a basic navigation menu item with minimal input', async () => {
|
||||
const targetRecordId = faker.string.uuid();
|
||||
|
||||
const { data } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
createdNavigationMenuItemId = data?.createNavigationMenuItem?.id;
|
||||
|
||||
expect(data.createNavigationMenuItem).toMatchObject({
|
||||
id: expect.any(String),
|
||||
targetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
userWorkspaceId: null,
|
||||
folderId: null,
|
||||
position: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create navigation menu item with all optional fields', async () => {
|
||||
const targetRecordId = faker.string.uuid();
|
||||
const folderTargetRecordId = faker.string.uuid();
|
||||
|
||||
const { data: folderData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId: folderTargetRecordId,
|
||||
targetObjectMetadataId: companyObjectMetadataId,
|
||||
userWorkspaceId: validUserWorkspaceId ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const folderId = folderData?.createNavigationMenuItem?.id;
|
||||
|
||||
jestExpectToBeDefined(folderId);
|
||||
createdFolderId = folderId;
|
||||
|
||||
const { data } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId,
|
||||
targetObjectMetadataId: companyObjectMetadataId,
|
||||
userWorkspaceId: validUserWorkspaceId ?? undefined,
|
||||
folderId,
|
||||
position: 5,
|
||||
},
|
||||
});
|
||||
|
||||
createdNavigationMenuItemId = data?.createNavigationMenuItem?.id;
|
||||
|
||||
expect(data.createNavigationMenuItem).toMatchObject({
|
||||
id: expect.any(String),
|
||||
targetRecordId,
|
||||
targetObjectMetadataId: companyObjectMetadataId,
|
||||
userWorkspaceId: validUserWorkspaceId,
|
||||
folderId,
|
||||
position: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should auto-calculate position when not provided', async () => {
|
||||
const targetRecordId1 = faker.string.uuid();
|
||||
const targetRecordId2 = faker.string.uuid();
|
||||
|
||||
const { data: data1 } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId: targetRecordId1,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const firstItemId = data1.createNavigationMenuItem.id;
|
||||
const firstPosition = data1.createNavigationMenuItem.position;
|
||||
|
||||
const { data: data2 } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId: targetRecordId2,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
createdNavigationMenuItemId = data2.createNavigationMenuItem.id;
|
||||
|
||||
expect(data2.createNavigationMenuItem.position).toBeGreaterThan(
|
||||
firstPosition,
|
||||
);
|
||||
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: firstItemId },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util';
|
||||
import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { findNavigationMenuItems } from 'test/integration/metadata/suites/navigation-menu-item/utils/find-navigation-menu-items.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
|
||||
describe('NavigationMenuItem deletion should succeed', () => {
|
||||
let personObjectMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: { first: 1000 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
`,
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(objects);
|
||||
|
||||
const personObjectMetadata = objects.find(
|
||||
(object: { nameSingular: string }) => object.nameSingular === 'person',
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(personObjectMetadata);
|
||||
|
||||
personObjectMetadataId = personObjectMetadata.id;
|
||||
});
|
||||
|
||||
it('should delete an existing navigation menu item', async () => {
|
||||
const targetRecordId = faker.string.uuid();
|
||||
|
||||
const { data: createData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const createdId = createData.createNavigationMenuItem.id;
|
||||
|
||||
const { data: deleteData } = await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: createdId },
|
||||
});
|
||||
|
||||
expect(deleteData.deleteNavigationMenuItem).toMatchObject({
|
||||
id: createdId,
|
||||
targetRecordId,
|
||||
});
|
||||
|
||||
const { data: findData } = await findNavigationMenuItems({
|
||||
expectToFail: false,
|
||||
input: undefined,
|
||||
});
|
||||
|
||||
const deletedItem = findData.navigationMenuItems.find(
|
||||
(item) => item.id === createdId,
|
||||
);
|
||||
|
||||
expect(deletedItem).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item.util';
|
||||
import { deleteNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item.util';
|
||||
import { updateNavigationMenuItem } from 'test/integration/metadata/suites/navigation-menu-item/utils/update-navigation-menu-item.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
|
||||
describe('NavigationMenuItem update should succeed', () => {
|
||||
let createdNavigationMenuItemId: string;
|
||||
let personObjectMetadataId: string;
|
||||
let createdFolderId: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {},
|
||||
paging: { first: 1000 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
`,
|
||||
});
|
||||
|
||||
jestExpectToBeDefined(objects);
|
||||
|
||||
const personObjectMetadata = objects.find(
|
||||
(object: { nameSingular: string }) => object.nameSingular === 'person',
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(personObjectMetadata);
|
||||
|
||||
personObjectMetadataId = personObjectMetadata.id;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const targetRecordId = faker.string.uuid();
|
||||
|
||||
const { data } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
position: 1,
|
||||
},
|
||||
});
|
||||
|
||||
createdNavigationMenuItemId = data.createNavigationMenuItem.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdNavigationMenuItemId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: createdNavigationMenuItemId },
|
||||
});
|
||||
createdNavigationMenuItemId = undefined as unknown as string;
|
||||
}
|
||||
if (createdFolderId) {
|
||||
await deleteNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: { id: createdFolderId },
|
||||
});
|
||||
createdFolderId = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should update the position', async () => {
|
||||
const { data } = await updateNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: createdNavigationMenuItemId,
|
||||
update: {
|
||||
position: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(data.updateNavigationMenuItem).toMatchObject({
|
||||
id: createdNavigationMenuItemId,
|
||||
position: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the folderId', async () => {
|
||||
const folderTargetRecordId = faker.string.uuid();
|
||||
|
||||
const { data: folderData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId: folderTargetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const folderId = folderData?.createNavigationMenuItem?.id;
|
||||
|
||||
jestExpectToBeDefined(folderId);
|
||||
createdFolderId = folderId;
|
||||
|
||||
const { data } = await updateNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: createdNavigationMenuItemId,
|
||||
update: {
|
||||
folderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(data.updateNavigationMenuItem).toMatchObject({
|
||||
id: createdNavigationMenuItemId,
|
||||
folderId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update folderId to null', async () => {
|
||||
const folderTargetRecordId = faker.string.uuid();
|
||||
|
||||
const { data: folderData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId: folderTargetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const folderId = folderData?.createNavigationMenuItem?.id;
|
||||
|
||||
jestExpectToBeDefined(folderId);
|
||||
createdFolderId = folderId;
|
||||
|
||||
await updateNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: createdNavigationMenuItemId,
|
||||
update: {
|
||||
folderId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await updateNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: createdNavigationMenuItemId,
|
||||
update: {
|
||||
folderId: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(data.updateNavigationMenuItem).toMatchObject({
|
||||
id: createdNavigationMenuItemId,
|
||||
folderId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update multiple fields at once', async () => {
|
||||
const folderTargetRecordId = faker.string.uuid();
|
||||
|
||||
const { data: folderData } = await createNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
targetRecordId: folderTargetRecordId,
|
||||
targetObjectMetadataId: personObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const folderId = folderData?.createNavigationMenuItem?.id;
|
||||
|
||||
jestExpectToBeDefined(folderId);
|
||||
createdFolderId = folderId;
|
||||
|
||||
const { data } = await updateNavigationMenuItem({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: createdNavigationMenuItemId,
|
||||
update: {
|
||||
folderId,
|
||||
position: 99,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(data.updateNavigationMenuItem).toMatchObject({
|
||||
id: createdNavigationMenuItemId,
|
||||
folderId,
|
||||
position: 99,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
|
||||
import { type CreateNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/create-navigation-menu-item.input';
|
||||
|
||||
export type CreateNavigationMenuItemFactoryInput =
|
||||
CreateNavigationMenuItemInput;
|
||||
|
||||
const DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS = `
|
||||
id
|
||||
userWorkspaceId
|
||||
targetRecordId
|
||||
targetObjectMetadataId
|
||||
folderId
|
||||
position
|
||||
applicationId
|
||||
createdAt
|
||||
updatedAt
|
||||
`;
|
||||
|
||||
export const createNavigationMenuItemQueryFactory = ({
|
||||
input,
|
||||
gqlFields = DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS,
|
||||
}: PerformMetadataQueryParams<CreateNavigationMenuItemFactoryInput>) => ({
|
||||
query: gql`
|
||||
mutation CreateNavigationMenuItem($input: CreateNavigationMenuItemInput!) {
|
||||
createNavigationMenuItem(input: $input) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
type CreateNavigationMenuItemFactoryInput,
|
||||
createNavigationMenuItemQueryFactory,
|
||||
} from 'test/integration/metadata/suites/navigation-menu-item/utils/create-navigation-menu-item-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
|
||||
export const createNavigationMenuItem = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: PerformMetadataQueryParams<CreateNavigationMenuItemFactoryInput>): CommonResponseBody<{
|
||||
createNavigationMenuItem: NavigationMenuItemDTO;
|
||||
}> => {
|
||||
const graphqlOperation = createNavigationMenuItemQueryFactory({
|
||||
input,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'NavigationMenuItem creation should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'NavigationMenuItem creation has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
|
||||
export type DeleteNavigationMenuItemFactoryInput = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS = `
|
||||
id
|
||||
targetRecordId
|
||||
position
|
||||
`;
|
||||
|
||||
export const deleteNavigationMenuItemQueryFactory = ({
|
||||
input,
|
||||
gqlFields = DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS,
|
||||
}: PerformMetadataQueryParams<DeleteNavigationMenuItemFactoryInput>) => ({
|
||||
query: gql`
|
||||
mutation DeleteNavigationMenuItem($id: UUID!) {
|
||||
deleteNavigationMenuItem(id: $id) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
type DeleteNavigationMenuItemFactoryInput,
|
||||
deleteNavigationMenuItemQueryFactory,
|
||||
} from 'test/integration/metadata/suites/navigation-menu-item/utils/delete-navigation-menu-item-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
|
||||
export const deleteNavigationMenuItem = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: PerformMetadataQueryParams<DeleteNavigationMenuItemFactoryInput>): CommonResponseBody<{
|
||||
deleteNavigationMenuItem: NavigationMenuItemDTO;
|
||||
}> => {
|
||||
const graphqlOperation = deleteNavigationMenuItemQueryFactory({
|
||||
input,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'NavigationMenuItem deletion should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'NavigationMenuItem deletion has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
|
||||
const DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS = `
|
||||
id
|
||||
userWorkspaceId
|
||||
targetRecordId
|
||||
targetObjectMetadataId
|
||||
folderId
|
||||
position
|
||||
applicationId
|
||||
createdAt
|
||||
updatedAt
|
||||
`;
|
||||
|
||||
export const findNavigationMenuItemsQueryFactory = ({
|
||||
gqlFields = DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS,
|
||||
}: PerformMetadataQueryParams<void>) => ({
|
||||
query: gql`
|
||||
query NavigationMenuItems {
|
||||
navigationMenuItems {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { findNavigationMenuItemsQueryFactory } from 'test/integration/metadata/suites/navigation-menu-item/utils/find-navigation-menu-items-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
|
||||
export const findNavigationMenuItems = async ({
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: PerformMetadataQueryParams<void>): CommonResponseBody<{
|
||||
navigationMenuItems: NavigationMenuItemDTO[];
|
||||
}> => {
|
||||
const graphqlOperation = findNavigationMenuItemsQueryFactory({
|
||||
input: undefined,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'NavigationMenuItems query should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'NavigationMenuItems query has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
|
||||
import { type UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input';
|
||||
|
||||
export type UpdateNavigationMenuItemFactoryInput =
|
||||
UpdateOneNavigationMenuItemInput;
|
||||
|
||||
const DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS = `
|
||||
id
|
||||
userWorkspaceId
|
||||
targetRecordId
|
||||
targetObjectMetadataId
|
||||
folderId
|
||||
position
|
||||
applicationId
|
||||
createdAt
|
||||
updatedAt
|
||||
`;
|
||||
|
||||
export const updateNavigationMenuItemQueryFactory = ({
|
||||
input,
|
||||
gqlFields = DEFAULT_NAVIGATION_MENU_ITEM_GQL_FIELDS,
|
||||
}: PerformMetadataQueryParams<UpdateNavigationMenuItemFactoryInput>) => ({
|
||||
query: gql`
|
||||
mutation UpdateNavigationMenuItem($input: UpdateOneNavigationMenuItemInput!) {
|
||||
updateNavigationMenuItem(input: $input) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
type UpdateNavigationMenuItemFactoryInput,
|
||||
updateNavigationMenuItemQueryFactory,
|
||||
} from 'test/integration/metadata/suites/navigation-menu-item/utils/update-navigation-menu-item-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-menu-item/dtos/navigation-menu-item.dto';
|
||||
|
||||
export const updateNavigationMenuItem = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: PerformMetadataQueryParams<UpdateNavigationMenuItemFactoryInput>): CommonResponseBody<{
|
||||
updateNavigationMenuItem: NavigationMenuItemDTO;
|
||||
}> => {
|
||||
const graphqlOperation = updateNavigationMenuItemQueryFactory({
|
||||
input,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'NavigationMenuItem update should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'NavigationMenuItem update has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -21,5 +21,6 @@ export const ALL_METADATA_NAME = {
|
||||
pageLayoutWidget: 'pageLayoutWidget',
|
||||
pageLayoutTab: 'pageLayoutTab',
|
||||
commandMenuItem: 'commandMenuItem',
|
||||
navigationMenuItem: 'navigationMenuItem',
|
||||
frontComponent: 'frontComponent',
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user