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:
+212
@@ -1,12 +1,16 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import {
|
||||
FieldMetadataType,
|
||||
OrderByDirection,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { ViewCalendarLayout } from 'src/engine/metadata-modules/view/enums/view-calendar-layout.enum';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewQueryParamsService } from 'src/engine/metadata-modules/view/services/view-query-params.service';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { ViewToolsFactory } from 'src/engine/metadata-modules/view/tools/view-tools.factory';
|
||||
@@ -14,6 +18,7 @@ import { ViewToolsFactory } from 'src/engine/metadata-modules/view/tools/view-to
|
||||
describe('ViewToolsFactory', () => {
|
||||
let viewToolsFactory: ViewToolsFactory;
|
||||
let viewService: jest.Mocked<ViewService>;
|
||||
let viewFieldService: jest.Mocked<ViewFieldService>;
|
||||
let viewQueryParamsService: jest.Mocked<ViewQueryParamsService>;
|
||||
let _flatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
|
||||
|
||||
@@ -22,6 +27,36 @@ describe('ViewToolsFactory', () => {
|
||||
const mockViewId = 'view-id';
|
||||
const mockObjectMetadataId = 'object-metadata-id';
|
||||
const mockObjectNameSingular = 'company';
|
||||
const mockCalendarFieldMetadataId = 'calendar-field-metadata-id';
|
||||
|
||||
const mockNameFieldMetadataId = 'name-field-metadata-id';
|
||||
const mockStageFieldMetadataId = 'stage-field-metadata-id';
|
||||
|
||||
const mockFlatFieldMetadataMaps = {
|
||||
byUniversalIdentifier: {
|
||||
'field-universal-id': {
|
||||
id: mockCalendarFieldMetadataId,
|
||||
name: 'dueAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
universalIdentifier: 'field-universal-id',
|
||||
},
|
||||
'name-field-universal-id': {
|
||||
id: mockNameFieldMetadataId,
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
universalIdentifier: 'name-field-universal-id',
|
||||
},
|
||||
'stage-field-universal-id': {
|
||||
id: mockStageFieldMetadataId,
|
||||
name: 'stage',
|
||||
type: FieldMetadataType.SELECT,
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
universalIdentifier: 'stage-field-universal-id',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockView = {
|
||||
id: mockViewId,
|
||||
@@ -70,6 +105,12 @@ describe('ViewToolsFactory', () => {
|
||||
deleteOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ViewFieldService,
|
||||
useValue: {
|
||||
createMany: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ViewQueryParamsService,
|
||||
useValue: {
|
||||
@@ -81,6 +122,7 @@ describe('ViewToolsFactory', () => {
|
||||
useValue: {
|
||||
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn().mockResolvedValue({
|
||||
flatObjectMetadataMaps: mockFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: mockFlatFieldMetadataMaps,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -89,6 +131,7 @@ describe('ViewToolsFactory', () => {
|
||||
|
||||
viewToolsFactory = module.get<ViewToolsFactory>(ViewToolsFactory);
|
||||
viewService = module.get(ViewService);
|
||||
viewFieldService = module.get(ViewFieldService);
|
||||
viewQueryParamsService = module.get(ViewQueryParamsService);
|
||||
_flatEntityMapsCacheService = module.get(
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
@@ -275,6 +318,175 @@ describe('ViewToolsFactory', () => {
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create view fields when fieldNames is provided', async () => {
|
||||
const createdView = {
|
||||
id: 'new-view-id',
|
||||
name: 'Kanban View',
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
type: ViewType.KANBAN,
|
||||
icon: 'IconLayoutKanban',
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
};
|
||||
|
||||
viewService.createOne.mockResolvedValue(createdView as any);
|
||||
|
||||
const tools = viewToolsFactory.generateWriteTools(
|
||||
mockWorkspaceId,
|
||||
mockUserWorkspaceId,
|
||||
);
|
||||
|
||||
await callExecute(tools['create_view'], {
|
||||
name: 'Kanban View',
|
||||
objectNameSingular: mockObjectNameSingular,
|
||||
icon: 'IconLayoutKanban',
|
||||
type: ViewType.KANBAN,
|
||||
mainGroupByFieldName: 'stage',
|
||||
fieldNames: ['name', 'stage'],
|
||||
});
|
||||
|
||||
expect(viewFieldService.createMany).toHaveBeenCalledWith({
|
||||
createViewFieldInputs: [
|
||||
{
|
||||
viewId: 'new-view-id',
|
||||
fieldMetadataId: mockNameFieldMetadataId,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
viewId: 'new-view-id',
|
||||
fieldMetadataId: mockStageFieldMetadataId,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when KANBAN view missing mainGroupByFieldName', async () => {
|
||||
const tools = viewToolsFactory.generateWriteTools(
|
||||
mockWorkspaceId,
|
||||
mockUserWorkspaceId,
|
||||
);
|
||||
|
||||
await expect(
|
||||
callExecute(tools['create_view'], {
|
||||
name: 'Kanban View',
|
||||
objectNameSingular: mockObjectNameSingular,
|
||||
type: ViewType.KANBAN,
|
||||
}),
|
||||
).rejects.toThrow('KANBAN views require mainGroupByFieldName');
|
||||
});
|
||||
|
||||
it('should throw when CALENDAR view missing calendarFieldName', async () => {
|
||||
const tools = viewToolsFactory.generateWriteTools(
|
||||
mockWorkspaceId,
|
||||
mockUserWorkspaceId,
|
||||
);
|
||||
|
||||
await expect(
|
||||
callExecute(tools['create_view'], {
|
||||
name: 'Calendar View',
|
||||
objectNameSingular: mockObjectNameSingular,
|
||||
type: ViewType.CALENDAR,
|
||||
calendarLayout: ViewCalendarLayout.WEEK,
|
||||
}),
|
||||
).rejects.toThrow('CALENDAR views require calendarFieldName');
|
||||
});
|
||||
|
||||
it('should throw when CALENDAR view missing calendarLayout', async () => {
|
||||
const tools = viewToolsFactory.generateWriteTools(
|
||||
mockWorkspaceId,
|
||||
mockUserWorkspaceId,
|
||||
);
|
||||
|
||||
await expect(
|
||||
callExecute(tools['create_view'], {
|
||||
name: 'Calendar View',
|
||||
objectNameSingular: mockObjectNameSingular,
|
||||
type: ViewType.CALENDAR,
|
||||
calendarFieldName: 'dueAt',
|
||||
}),
|
||||
).rejects.toThrow('CALENDAR views require calendarLayout');
|
||||
});
|
||||
|
||||
it('should not create view fields when fieldNames is not provided', async () => {
|
||||
const createdView = {
|
||||
id: 'new-view-id',
|
||||
name: 'New View',
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
type: ViewType.TABLE,
|
||||
icon: 'IconTable',
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
};
|
||||
|
||||
viewService.createOne.mockResolvedValue(createdView as any);
|
||||
|
||||
const tools = viewToolsFactory.generateWriteTools(
|
||||
mockWorkspaceId,
|
||||
mockUserWorkspaceId,
|
||||
);
|
||||
|
||||
await callExecute(tools['create_view'], {
|
||||
name: 'New View',
|
||||
objectNameSingular: mockObjectNameSingular,
|
||||
icon: 'IconTable',
|
||||
});
|
||||
|
||||
expect(viewFieldService.createMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a calendar view with layout and field', async () => {
|
||||
const createdView = {
|
||||
id: 'new-calendar-view-id',
|
||||
name: 'Calendar View',
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
type: ViewType.CALENDAR,
|
||||
icon: 'IconCalendar',
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
};
|
||||
|
||||
viewService.createOne.mockResolvedValue(createdView as any);
|
||||
|
||||
const tools = viewToolsFactory.generateWriteTools(
|
||||
mockWorkspaceId,
|
||||
mockUserWorkspaceId,
|
||||
);
|
||||
|
||||
const result = await callExecute(tools['create_view'], {
|
||||
name: 'Calendar View',
|
||||
objectNameSingular: mockObjectNameSingular,
|
||||
icon: 'IconCalendar',
|
||||
type: ViewType.CALENDAR,
|
||||
calendarLayout: ViewCalendarLayout.WEEK,
|
||||
calendarFieldName: 'dueAt',
|
||||
});
|
||||
|
||||
expect(viewService.createOne).toHaveBeenCalledWith({
|
||||
createViewInput: {
|
||||
name: 'Calendar View',
|
||||
objectMetadataId: mockObjectMetadataId,
|
||||
icon: 'IconCalendar',
|
||||
type: ViewType.CALENDAR,
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
calendarLayout: ViewCalendarLayout.WEEK,
|
||||
calendarFieldMetadataId: mockCalendarFieldMetadataId,
|
||||
},
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdByUserWorkspaceId: mockUserWorkspaceId,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: 'new-calendar-view-id',
|
||||
name: 'Calendar View',
|
||||
objectNameSingular: mockObjectNameSingular,
|
||||
type: ViewType.CALENDAR,
|
||||
icon: 'IconCalendar',
|
||||
visibility: ViewVisibility.WORKSPACE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update-view tool', () => {
|
||||
|
||||
+171
-7
@@ -1,19 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
AggregateOperations,
|
||||
FieldMetadataType,
|
||||
ViewType,
|
||||
ViewVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { formatValidationErrors } from 'src/engine/core-modules/tool-provider/utils/format-validation-errors.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
|
||||
import { ViewCalendarLayout } from 'src/engine/metadata-modules/view/enums/view-calendar-layout.enum';
|
||||
import { ViewQueryParamsService } from 'src/engine/metadata-modules/view/services/view-query-params.service';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { isFieldMetadataDateKind, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
const GetViewsInputSchema = z.object({
|
||||
objectNameSingular: z
|
||||
@@ -76,6 +81,28 @@ const CreateViewInputSchema = z.object({
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Field name for the kanban aggregate operation (e.g., "amount")'),
|
||||
calendarLayout: z
|
||||
.enum([
|
||||
ViewCalendarLayout.DAY,
|
||||
ViewCalendarLayout.WEEK,
|
||||
ViewCalendarLayout.MONTH,
|
||||
])
|
||||
.optional()
|
||||
.describe(
|
||||
'Calendar layout (required for CALENDAR views, e.g., "DAY", "WEEK", "MONTH")',
|
||||
),
|
||||
calendarFieldName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Date field name to use for the calendar (required for CALENDAR views, must be a DATE or DATE_TIME field, e.g., "createdAt", "dueAt")',
|
||||
),
|
||||
fieldNames: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Field names to display in the view as columns (for TABLE) or cards (for KANBAN/CALENDAR). Fields are displayed in the order provided. Use get_field_metadata to find available field names.',
|
||||
),
|
||||
});
|
||||
|
||||
const UpdateViewInputSchema = z.object({
|
||||
@@ -92,6 +119,7 @@ const DeleteViewInputSchema = z.object({
|
||||
export class ViewToolsFactory {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly viewQueryParamsService: ViewQueryParamsService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
@@ -153,6 +181,78 @@ export class ViewToolsFactory {
|
||||
return fieldMetadata.id;
|
||||
}
|
||||
|
||||
private async resolveGroupByFieldMetadataId(
|
||||
workspaceId: string,
|
||||
objectMetadataId: string,
|
||||
fieldName: string,
|
||||
): Promise<string> {
|
||||
const { flatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const fieldMetadata = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
).find(
|
||||
(field) =>
|
||||
field?.name === fieldName &&
|
||||
field?.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new Error(
|
||||
`Field "${fieldName}" not found on this object. Use get_field_metadata to list available fields.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadata.type !== FieldMetadataType.SELECT) {
|
||||
throw new Error(
|
||||
`Field "${fieldName}" has type "${fieldMetadata.type}" and cannot be used as a group-by field. Only SELECT fields are supported for grouping (board columns and table groups).`,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadata.id;
|
||||
}
|
||||
|
||||
private async resolveCalendarFieldMetadataId(
|
||||
workspaceId: string,
|
||||
objectMetadataId: string,
|
||||
fieldName: string,
|
||||
): Promise<string> {
|
||||
const { flatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const fieldMetadata = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
).find(
|
||||
(field) =>
|
||||
field?.name === fieldName &&
|
||||
field?.objectMetadataId === objectMetadataId,
|
||||
);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new Error(
|
||||
`Field "${fieldName}" not found on this object. Use get_field_metadata to list available fields.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFieldMetadataDateKind(fieldMetadata.type)) {
|
||||
throw new Error(
|
||||
`Field "${fieldName}" has type "${fieldMetadata.type}" and cannot be used as a calendar field. Only DATE or DATE_TIME fields are supported.`,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadata.id;
|
||||
}
|
||||
|
||||
generateReadTools(
|
||||
workspaceId: string,
|
||||
userWorkspaceId?: string,
|
||||
@@ -219,7 +319,7 @@ export class ViewToolsFactory {
|
||||
return {
|
||||
create_view: {
|
||||
description:
|
||||
'Create a new view for an object. Views define how records are displayed. For KANBAN views, mainGroupByFieldName is required and must be a SELECT field (e.g., "stage", "status").',
|
||||
'Create a new view for an object. Views define how records are displayed. For KANBAN views, mainGroupByFieldName is required and must be a SELECT field (e.g., "stage", "status"). For CALENDAR views, calendarFieldName and calendarLayout are required.',
|
||||
inputSchema: CreateViewInputSchema,
|
||||
execute: async (parameters: {
|
||||
name: string;
|
||||
@@ -230,6 +330,9 @@ export class ViewToolsFactory {
|
||||
mainGroupByFieldName?: string;
|
||||
kanbanAggregateOperation?: string;
|
||||
kanbanAggregateOperationFieldName?: string;
|
||||
calendarLayout?: ViewCalendarLayout;
|
||||
calendarFieldName?: string;
|
||||
fieldNames?: string[];
|
||||
}) => {
|
||||
try {
|
||||
const objectMetadataId = await this.resolveObjectMetadataId(
|
||||
@@ -237,15 +340,40 @@ export class ViewToolsFactory {
|
||||
parameters.objectNameSingular,
|
||||
);
|
||||
|
||||
if (
|
||||
parameters.type === ViewType.KANBAN &&
|
||||
!parameters.mainGroupByFieldName
|
||||
) {
|
||||
throw new Error(
|
||||
'KANBAN views require mainGroupByFieldName. Provide a SELECT field name (e.g., "stage", "status") to group records into columns.',
|
||||
);
|
||||
}
|
||||
|
||||
if (parameters.type === ViewType.CALENDAR) {
|
||||
if (!parameters.calendarFieldName) {
|
||||
throw new Error(
|
||||
'CALENDAR views require calendarFieldName. Provide a DATE or DATE_TIME field name (e.g., "dueAt", "createdAt").',
|
||||
);
|
||||
}
|
||||
|
||||
if (!parameters.calendarLayout) {
|
||||
throw new Error(
|
||||
'CALENDAR views require calendarLayout. Provide one of: "DAY", "WEEK", "MONTH".',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mainGroupByFieldMetadataId: string | undefined;
|
||||
let kanbanAggregateOperationFieldMetadataId: string | undefined;
|
||||
let calendarFieldMetadataId: string | undefined;
|
||||
|
||||
if (parameters.mainGroupByFieldName) {
|
||||
mainGroupByFieldMetadataId = await this.resolveFieldMetadataId(
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
parameters.mainGroupByFieldName,
|
||||
);
|
||||
mainGroupByFieldMetadataId =
|
||||
await this.resolveGroupByFieldMetadataId(
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
parameters.mainGroupByFieldName,
|
||||
);
|
||||
}
|
||||
|
||||
if (parameters.kanbanAggregateOperationFieldName) {
|
||||
@@ -257,6 +385,15 @@ export class ViewToolsFactory {
|
||||
);
|
||||
}
|
||||
|
||||
if (parameters.calendarFieldName) {
|
||||
calendarFieldMetadataId =
|
||||
await this.resolveCalendarFieldMetadataId(
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
parameters.calendarFieldName,
|
||||
);
|
||||
}
|
||||
|
||||
const view = await this.viewService.createOne({
|
||||
createViewInput: {
|
||||
name: parameters.name,
|
||||
@@ -268,11 +405,38 @@ export class ViewToolsFactory {
|
||||
kanbanAggregateOperation:
|
||||
parameters.kanbanAggregateOperation as AggregateOperations,
|
||||
kanbanAggregateOperationFieldMetadataId,
|
||||
calendarLayout: parameters.calendarLayout,
|
||||
calendarFieldMetadataId,
|
||||
},
|
||||
workspaceId,
|
||||
createdByUserWorkspaceId: userWorkspaceId,
|
||||
});
|
||||
|
||||
if (isNonEmptyArray(parameters.fieldNames)) {
|
||||
const resolvedFieldMetadataIds = await Promise.all(
|
||||
parameters.fieldNames.map((fieldName) =>
|
||||
this.resolveFieldMetadataId(
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
fieldName,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await this.viewFieldService.createMany({
|
||||
createViewFieldInputs: resolvedFieldMetadataIds.map(
|
||||
(fieldMetadataId, index) => ({
|
||||
viewId: view.id,
|
||||
fieldMetadataId,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
position: index,
|
||||
}),
|
||||
),
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
|
||||
Reference in New Issue
Block a user