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:
+55
-40
@@ -19,6 +19,7 @@ import {
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
@@ -182,66 +183,80 @@ export class NavigateAppTool implements Tool {
|
||||
},
|
||||
);
|
||||
|
||||
const availableObjectNames = navigationMenuItems
|
||||
.map((navigationMenuItem) => {
|
||||
if (isDefined(navigationMenuItem.viewId)) {
|
||||
const correspondingViewUniversalIdentifier =
|
||||
flatViewMaps.universalIdentifierById[navigationMenuItem.viewId];
|
||||
type NavigatableObject = {
|
||||
nameSingular: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
};
|
||||
|
||||
if (isDefined(correspondingViewUniversalIdentifier)) {
|
||||
const correspondingView =
|
||||
flatViewMaps.byUniversalIdentifier[
|
||||
correspondingViewUniversalIdentifier
|
||||
];
|
||||
const navigatableObjects = navigationMenuItems
|
||||
.map<NavigatableObject | null>((navigationMenuItem) => {
|
||||
const objectMetadataId =
|
||||
navigationMenuItem.type === NavigationMenuItemType.OBJECT
|
||||
? navigationMenuItem.targetObjectMetadataId
|
||||
: navigationMenuItem.type === NavigationMenuItemType.VIEW &&
|
||||
isDefined(navigationMenuItem.viewId)
|
||||
? flatViewMaps.byUniversalIdentifier[
|
||||
flatViewMaps.universalIdentifierById[
|
||||
navigationMenuItem.viewId
|
||||
] ?? ''
|
||||
]?.objectMetadataId
|
||||
: undefined;
|
||||
|
||||
if (isDefined(correspondingView)) {
|
||||
const correspondingObjectMetadataUniversalIdentifier =
|
||||
flatObjectMetadataMaps.universalIdentifierById[
|
||||
correspondingView.objectMetadataId
|
||||
];
|
||||
|
||||
if (isDefined(correspondingObjectMetadataUniversalIdentifier)) {
|
||||
const correspondingObjectMetadata =
|
||||
flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
correspondingObjectMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (isDefined(correspondingObjectMetadata)) {
|
||||
const correspondingObjectNameSingular =
|
||||
correspondingObjectMetadata.nameSingular;
|
||||
|
||||
return correspondingObjectNameSingular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isDefined(objectMetadataId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
const objectMetadataUniversalIdentifier =
|
||||
flatObjectMetadataMaps.universalIdentifierById[objectMetadataId];
|
||||
|
||||
if (!isDefined(objectMetadataUniversalIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
objectMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(objectMetadata)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
nameSingular: objectMetadata.nameSingular,
|
||||
labelSingular: objectMetadata.labelSingular,
|
||||
labelPlural: objectMetadata.labelPlural,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const fuse = new Fuse(availableObjectNames, {
|
||||
threshold: 0.6,
|
||||
const fuse = new Fuse(navigatableObjects, {
|
||||
keys: ['nameSingular', 'labelSingular', 'labelPlural'],
|
||||
threshold: 0.4,
|
||||
});
|
||||
|
||||
const results = fuse.search(objectNameSingular.replace(/\s/g, ''));
|
||||
const firstMatchingNavigationItemLabel = results[0]?.item;
|
||||
const results = fuse.search(objectNameSingular);
|
||||
const matchingObject = results[0]?.item;
|
||||
|
||||
if (!isDefined(matchingObject)) {
|
||||
const availableLabels = navigatableObjects
|
||||
.map((object) => object.labelPlural)
|
||||
.join(', ');
|
||||
|
||||
if (!isDefined(firstMatchingNavigationItemLabel)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Object "${objectNameSingular}" not found`,
|
||||
error: `No object with singular name "${objectNameSingular}" was found in this workspace. Available objects: ${availableObjectNames}`,
|
||||
error: `No object matching "${objectNameSingular}" was found in this workspace. Available objects: ${availableLabels}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Navigating to ${firstMatchingNavigationItemLabel} default view`,
|
||||
message: `Navigating to ${matchingObject.labelPlural} default view`,
|
||||
result: {
|
||||
action: 'navigateToObject',
|
||||
objectNameSingular: firstMatchingNavigationItemLabel,
|
||||
objectNameSingular: matchingObject.nameSingular,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user