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:
+219
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user