Refined demo workspace creation skill (rebased, review fixes) (#19274)

## Summary

Rebased version of #19051 with all review comments addressed. Clean
branch on latest main, lint/typecheck/tests passing.

### Changes from original PR
- AI can now create, update, and delete **view filters**
(`ViewFilterToolsFactory`) and **view sorts** (`ViewSortToolsFactory`)
- `create_view` now accepts `calendarFieldName`, `calendarLayout`, and
`fieldNames` to configure views at creation time
- Three new standard skills: `view-building`, `view-filters-and-sorts`,
`custom-objects-cleanup`
- `workspace-demo-seeding` skill reworked to keep standard objects and
enrich them with custom fields
- Cache invalidation for nav menu items when object `isActive` changes
- Dashboard tool descriptions improved (RECORD_TABLE widget workflow)

### Review comments addressed (all 10 from #19051)
1. **Sentry + Cubic**: Calendar field DATE/DATE_TIME validation — added
`resolveCalendarFieldMetadataId` using `isFieldMetadataDateKind`
2. **Cubic**: "navigate tool" → "navigate_app tool" in skill metadata
(all 7 occurrences)
3. **Copilot**: KANBAN views now require `mainGroupByFieldName` — throws
clear error if missing
4. **Copilot**: CALENDAR views now require both `calendarFieldName` and
`calendarLayout` — validated before DB call
5. **Copilot**: Mock field fixtures include `type` property (DATE_TIME,
TEXT, SELECT)
6. **Copilot**: `ViewFilterValue` type assertion instead of unsafe `as
string` casts (3 locations)
7. **FelixMalfait**: Removed
`NavigationMenuItemObjectDeactivationListener` — replaced with cache
invalidation
8. **FelixMalfait**: Consolidated `ViewFilterToolProvider` and
`ViewSortToolProvider` into single `ViewToolProvider`
9. Removed `VIEW_FILTER` and `VIEW_SORT` from `ToolCategory` enum
(merged into `VIEW`)
10. Removed stale `existingFeatureFlagsMap` param incompatible with
current main

## Test plan
- [x] `npx nx lint:diff-with-main twenty-server` — passes
- [x] `npx nx typecheck twenty-server` — passes
- [x] `view-tools.factory.spec.ts` — all 20 tests pass (including 3 new
validation tests)

Supersedes #19051

https://claude.ai/code/session_01QPV74NU6vzmJb32e4i899E

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-04-03 09:19:46 +02:00
committed by GitHub
parent 12b031b67d
commit bb3d556799
20 changed files with 1689 additions and 360 deletions
@@ -0,0 +1,219 @@
import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { z } from 'zod';
import { formatValidationErrors } from 'src/engine/core-modules/tool-provider/utils/format-validation-errors.util';
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
const VIEW_SORT_DIRECTION_OPTIONS = Object.values(ViewSortDirection);
const GetViewSortsInputSchema = z.object({
viewId: z
.string()
.uuid()
.describe('ID of the view to list sorts for. Obtain this from get_views.'),
});
const CreateViewSortInputSchema = z.object({
viewId: z.string().uuid().describe('ID of the view to add the sort to'),
fieldMetadataId: z
.string()
.uuid()
.describe(
'ID of the field to sort by. Use list_object_metadata_items to find field IDs.',
),
direction: z
.enum(VIEW_SORT_DIRECTION_OPTIONS)
.default(ViewSortDirection.ASC)
.describe('Sort direction: ASC (ascending) or DESC (descending)'),
});
const CreateManyViewSortsInputSchema = z.object({
sorts: z
.array(CreateViewSortInputSchema)
.min(1)
.max(10)
.describe('Array of sorts to create (1-10 items)'),
});
const UpdateViewSortInputSchema = z.object({
id: z.string().uuid().describe('ID of the view sort to update'),
direction: z
.enum(VIEW_SORT_DIRECTION_OPTIONS)
.optional()
.describe('New sort direction: ASC or DESC'),
});
const DeleteViewSortInputSchema = z.object({
id: z.string().uuid().describe('ID of the view sort to delete'),
});
@Injectable()
export class ViewSortToolsFactory {
constructor(private readonly viewSortService: ViewSortService) {}
generateReadTools(workspaceId: string): ToolSet {
return {
get_view_sorts: {
description:
'List all sorts applied to a view. Each sort defines a field and direction that determines the order records appear in the view.',
inputSchema: GetViewSortsInputSchema,
execute: async (parameters: { viewId: string }) => {
const sorts = await this.viewSortService.findByViewId(
workspaceId,
parameters.viewId,
);
return sorts.map((sort) => ({
id: sort.id,
viewId: sort.viewId,
fieldMetadataId: sort.fieldMetadataId,
direction: sort.direction,
}));
},
},
};
}
generateWriteTools(workspaceId: string): ToolSet {
return {
create_view_sort: {
description:
'Add a sort to a view. Use list_object_metadata_items to get fieldMetadataId values.',
inputSchema: CreateViewSortInputSchema,
execute: async (parameters: {
viewId: string;
fieldMetadataId: string;
direction: ViewSortDirection;
}) => {
try {
const sort = await this.viewSortService.createOne({
createViewSortInput: {
viewId: parameters.viewId,
fieldMetadataId: parameters.fieldMetadataId,
direction: parameters.direction,
},
workspaceId,
});
return {
id: sort.id,
viewId: sort.viewId,
fieldMetadataId: sort.fieldMetadataId,
direction: sort.direction,
};
} catch (error) {
if (error instanceof WorkspaceMigrationBuilderException) {
throw new Error(formatValidationErrors(error));
}
throw error;
}
},
},
create_many_view_sorts: {
description:
'Add multiple sorts to a view in one call. Use list_object_metadata_items to get fieldMetadataId values.',
inputSchema: CreateManyViewSortsInputSchema,
execute: async (parameters: {
sorts: Array<{
viewId: string;
fieldMetadataId: string;
direction: ViewSortDirection;
}>;
}) => {
const results = [];
for (const sortInput of parameters.sorts) {
try {
const sort = await this.viewSortService.createOne({
createViewSortInput: {
viewId: sortInput.viewId,
fieldMetadataId: sortInput.fieldMetadataId,
direction: sortInput.direction,
},
workspaceId,
});
results.push({
id: sort.id,
viewId: sort.viewId,
fieldMetadataId: sort.fieldMetadataId,
direction: sort.direction,
});
} catch (error) {
if (error instanceof WorkspaceMigrationBuilderException) {
throw new Error(formatValidationErrors(error));
}
throw error;
}
}
return { created: results };
},
},
update_view_sort: {
description:
'Update a sort on a view. Use get_view_sorts to find the sort ID.',
inputSchema: UpdateViewSortInputSchema,
execute: async (parameters: {
id: string;
direction?: ViewSortDirection;
}) => {
try {
const sort = await this.viewSortService.updateOne({
updateViewSortInput: {
id: parameters.id,
update: {
direction: parameters.direction,
},
},
workspaceId,
});
return {
id: sort.id,
viewId: sort.viewId,
fieldMetadataId: sort.fieldMetadataId,
direction: sort.direction,
};
} catch (error) {
if (error instanceof WorkspaceMigrationBuilderException) {
throw new Error(formatValidationErrors(error));
}
throw error;
}
},
},
delete_view_sort: {
description:
'Remove a sort from a view. Use get_view_sorts to find the sort ID.',
inputSchema: DeleteViewSortInputSchema,
execute: async (parameters: { id: string }) => {
try {
const sort = await this.viewSortService.deleteOne({
deleteViewSortInput: { id: parameters.id },
workspaceId,
});
return {
id: sort.id,
deleted: true,
};
} catch (error) {
if (error instanceof WorkspaceMigrationBuilderException) {
throw new Error(formatValidationErrors(error));
}
throw error;
}
},
},
};
}
}
@@ -8,6 +8,7 @@ import { ViewSortController } from 'src/engine/metadata-modules/view-sort/contro
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
import { ViewSortResolver } from 'src/engine/metadata-modules/view-sort/resolvers/view-sort.resolver';
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
import { ViewSortToolsFactory } from 'src/engine/metadata-modules/view-sort/tools/view-sort-tools.factory';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@@ -24,7 +25,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
ViewPermissionsModule,
],
controllers: [ViewSortController],
providers: [ViewSortService, ViewSortResolver],
exports: [ViewSortService],
providers: [ViewSortService, ViewSortResolver, ViewSortToolsFactory],
exports: [ViewSortService, ViewSortToolsFactory],
})
export class ViewSortModule {}