Address PR review comments: consolidate view tool providers, add validations

- Consolidate ViewFilterToolProvider and ViewSortToolProvider into ViewToolProvider
- Remove VIEW_FILTER and VIEW_SORT tool categories
- Remove NavigationMenuItemObjectDeactivationListener (prefer display-time filtering)
- Validate calendarFieldName is DATE/DATE_TIME field type
- Enforce mainGroupByFieldName for KANBAN views
- Enforce calendarFieldName + calendarLayout for CALENDAR views
- Fix "navigate tool" -> "navigate_app tool" in skill metadata
- Fix ViewFilterValue type assertions (was incorrectly cast as string)
- Add type property to mock field metadata fixtures in tests
- Add validation tests for KANBAN and CALENDAR view requirements
This commit is contained in:
Félix Malfait
2026-04-02 15:32:06 +02:00
parent 35bbb917bc
commit c6588694a9
5 changed files with 41 additions and 111 deletions
@@ -6,8 +6,6 @@ export enum ToolCategory {
NATIVE_MODEL = 'NATIVE_MODEL',
VIEW = 'VIEW',
VIEW_FIELD = 'VIEW_FIELD',
VIEW_FILTER = 'VIEW_FILTER',
VIEW_SORT = 'VIEW_SORT',
DASHBOARD = 'DASHBOARD',
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
}
@@ -16,6 +16,8 @@ import {
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { ViewFilterToolsFactory } from 'src/engine/metadata-modules/view-filter/tools/view-filter-tools.factory';
import { ViewSortToolsFactory } from 'src/engine/metadata-modules/view-sort/tools/view-sort-tools.factory';
import { ViewToolsFactory } from 'src/engine/metadata-modules/view/tools/view-tools.factory';
@Injectable()
@@ -24,23 +26,31 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
constructor(
private readonly viewToolsFactory: ViewToolsFactory,
private readonly viewFilterToolsFactory: ViewFilterToolsFactory,
private readonly viewSortToolsFactory: ViewSortToolsFactory,
private readonly permissionsService: PermissionsService,
private readonly toolExecutorService: ToolExecutorService,
) {}
onModuleInit(): void {
const factory = this.viewToolsFactory;
const viewFactory = this.viewToolsFactory;
const filterFactory = this.viewFilterToolsFactory;
const sortFactory = this.viewSortToolsFactory;
this.toolExecutorService.registerCategoryGenerator(
ToolCategory.VIEW,
async (context) => {
const workspaceMemberId = context.actorContext?.workspaceMemberId;
const readTools = factory.generateReadTools(
context.workspaceId,
workspaceMemberId ?? undefined,
workspaceMemberId ?? undefined,
);
const readTools = {
...viewFactory.generateReadTools(
context.workspaceId,
workspaceMemberId ?? undefined,
workspaceMemberId ?? undefined,
),
...filterFactory.generateReadTools(context.workspaceId),
...sortFactory.generateReadTools(context.workspaceId),
};
const hasViewPermission =
await this.permissionsService.checkRolesPermissions(
@@ -50,10 +60,14 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
);
if (hasViewPermission) {
const writeTools = factory.generateWriteTools(
context.workspaceId,
workspaceMemberId ?? undefined,
);
const writeTools = {
...viewFactory.generateWriteTools(
context.workspaceId,
workspaceMemberId ?? undefined,
),
...filterFactory.generateWriteTools(context.workspaceId),
...sortFactory.generateWriteTools(context.workspaceId),
};
return { ...readTools, ...writeTools };
}
@@ -76,11 +90,15 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
includeSchemas: options?.includeSchemas ?? true,
};
const readTools = this.viewToolsFactory.generateReadTools(
context.workspaceId,
workspaceMemberId ?? undefined,
workspaceMemberId ?? undefined,
);
const readTools = {
...this.viewToolsFactory.generateReadTools(
context.workspaceId,
workspaceMemberId ?? undefined,
workspaceMemberId ?? undefined,
),
...this.viewFilterToolsFactory.generateReadTools(context.workspaceId),
...this.viewSortToolsFactory.generateReadTools(context.workspaceId),
};
const hasViewPermission =
await this.permissionsService.checkRolesPermissions(
@@ -90,10 +108,14 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
);
if (hasViewPermission) {
const writeTools = this.viewToolsFactory.generateWriteTools(
context.workspaceId,
workspaceMemberId ?? undefined,
);
const writeTools = {
...this.viewToolsFactory.generateWriteTools(
context.workspaceId,
workspaceMemberId ?? undefined,
),
...this.viewFilterToolsFactory.generateWriteTools(context.workspaceId),
...this.viewSortToolsFactory.generateWriteTools(context.workspaceId),
};
return toolSetToDescriptors(
{ ...readTools, ...writeTools },
@@ -11,8 +11,6 @@ import { LogicFunctionToolProvider } from 'src/engine/core-modules/tool-provider
import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider';
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
import { ViewFieldToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-field-tool.provider';
import { ViewFilterToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-filter-tool.provider';
import { ViewSortToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-sort-tool.provider';
import { ViewToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-tool.provider';
import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider';
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
@@ -70,8 +68,6 @@ import { ToolRegistryService } from './services/tool-registry.service';
NativeModelToolProvider,
LogicFunctionToolProvider,
ViewFieldToolProvider,
ViewFilterToolProvider,
ViewSortToolProvider,
ViewToolProvider,
WorkflowToolProvider,
{
@@ -85,8 +81,6 @@ import { ToolRegistryService } from './services/tool-registry.service';
metadataProvider: MetadataToolProvider,
logicFunctionProvider: LogicFunctionToolProvider,
viewFieldProvider: ViewFieldToolProvider,
viewFilterProvider: ViewFilterToolProvider,
viewSortProvider: ViewSortToolProvider,
viewProvider: ViewToolProvider,
workflowProvider: WorkflowToolProvider,
) => [
@@ -96,8 +90,6 @@ import { ToolRegistryService } from './services/tool-registry.service';
metadataProvider,
logicFunctionProvider,
viewFieldProvider,
viewFilterProvider,
viewSortProvider,
viewProvider,
workflowProvider,
],
@@ -108,8 +100,6 @@ import { ToolRegistryService } from './services/tool-registry.service';
MetadataToolProvider,
LogicFunctionToolProvider,
ViewFieldToolProvider,
ViewFilterToolProvider,
ViewSortToolProvider,
ViewToolProvider,
WorkflowToolProvider,
],
@@ -7,7 +7,6 @@ import { FlatNavigationMenuItemModule } from 'src/engine/metadata-modules/flat-n
import { NavigationMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/navigation-menu-item/interceptors/navigation-menu-item-graphql-api-exception.interceptor';
import { NavigationMenuItemDeletionJob } from 'src/engine/metadata-modules/navigation-menu-item/jobs/navigation-menu-item-deletion.job';
import { NavigationMenuItemDeletionListener } from 'src/engine/metadata-modules/navigation-menu-item/listeners/navigation-menu-item-deletion.listener';
import { NavigationMenuItemObjectDeactivationListener } from 'src/engine/metadata-modules/navigation-menu-item/listeners/navigation-menu-item-object-deactivation.listener';
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';
@@ -31,7 +30,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
NavigationMenuItemAccessService,
NavigationMenuItemDeletionService,
NavigationMenuItemDeletionListener,
NavigationMenuItemObjectDeactivationListener,
NavigationMenuItemDeletionJob,
NavigationMenuItemResolver,
NavigationMenuItemRecordIdentifierService,
@@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
@@ -23,14 +22,6 @@ const isNavigationMenuItemForDeletedView = (
deletedIdsSet: Set<string>,
): boolean => isDefined(item.viewId) && deletedIdsSet.has(item.viewId);
const isNavigationMenuItemForDeactivatedObject = (
item: FlatNavigationMenuItem,
deactivatedObjectMetadataIdsSet: Set<string>,
): boolean =>
item.type === NavigationMenuItemType.OBJECT &&
isDefined(item.targetObjectMetadataId) &&
deactivatedObjectMetadataIdsSet.has(item.targetObjectMetadataId);
@Injectable()
export class NavigationMenuItemDeletionService {
constructor(
@@ -103,73 +94,4 @@ export class NavigationMenuItemDeletionService {
);
}
}
async deleteNavigationMenuItemsForDeactivatedObjects(
deactivatedObjectMetadataIds: string[],
workspaceId: string,
): Promise<void> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatNavigationMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatNavigationMenuItemMaps'],
},
);
const deactivatedObjectMetadataIdsSet = new Set(
deactivatedObjectMetadataIds,
);
const navigationMenuItemsToDelete = Object.values(
flatNavigationMenuItemMaps.byUniversalIdentifier,
).filter(
(item): item is NonNullable<typeof item> =>
isDefined(item) &&
isNavigationMenuItemForDeactivatedObject(
item,
deactivatedObjectMetadataIdsSet,
),
);
if (navigationMenuItemsToDelete.length === 0) {
return;
}
const flatNavigationMenuItemsToDelete = navigationMenuItemsToDelete.map(
(item) =>
fromDeleteNavigationMenuItemInputToFlatNavigationMenuItemOrThrow({
flatNavigationMenuItemMaps,
navigationMenuItemId: item.id,
}),
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
navigationMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: flatNavigationMenuItemsToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting navigation menu items for deactivated objects',
);
}
}
}