diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 34a78fa083..32cdb3de55 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3109,6 +3109,8 @@ type Query { getView(id: String!): View getViewSorts(viewId: String): [ViewSort!]! getViewSort(id: String!): ViewSort + getViewFields(viewId: String!): [ViewField!]! + getViewField(id: String!): ViewField getViewFieldGroups(viewId: String!): [ViewFieldGroup!]! getViewFieldGroup(id: String!): ViewFieldGroup apiKeys: [ApiKey!]! @@ -3147,8 +3149,6 @@ type Query { """Specify to filter the records returned.""" filter: ObjectFilter! = {} ): ObjectConnection! - getViewFields(viewId: String!): [ViewField!]! - getViewField(id: String!): ViewField index( """The id of the record to find.""" id: UUID! @@ -3401,6 +3401,11 @@ type Mutation { updateViewSort(input: UpdateViewSortInput!): ViewSort! deleteViewSort(input: DeleteViewSortInput!): Boolean! destroyViewSort(input: DestroyViewSortInput!): Boolean! + updateViewField(input: UpdateViewFieldInput!): ViewField! + createViewField(input: CreateViewFieldInput!): ViewField! + createManyViewFields(inputs: [CreateViewFieldInput!]!): [ViewField!]! + deleteViewField(input: DeleteViewFieldInput!): ViewField! + destroyViewField(input: DestroyViewFieldInput!): ViewField! updateViewFieldGroup(input: UpdateViewFieldGroupInput!): ViewFieldGroup! createViewFieldGroup(input: CreateViewFieldGroupInput!): ViewFieldGroup! createManyViewFieldGroups(inputs: [CreateViewFieldGroupInput!]!): [ViewFieldGroup!]! @@ -3454,11 +3459,6 @@ type Mutation { createOneObject(input: CreateOneObjectInput!): Object! deleteOneObject(input: DeleteOneObjectInput!): Object! updateOneObject(input: UpdateOneObjectInput!): Object! - updateViewField(input: UpdateViewFieldInput!): ViewField! - createViewField(input: CreateViewFieldInput!): ViewField! - createManyViewFields(inputs: [CreateViewFieldInput!]!): [ViewField!]! - deleteViewField(input: DeleteViewFieldInput!): ViewField! - destroyViewField(input: DestroyViewFieldInput!): ViewField! createOneAgent(input: CreateAgentInput!): Agent! updateOneAgent(input: UpdateAgentInput!): Agent! deleteOneAgent(input: AgentIdInput!): Agent! @@ -3752,6 +3752,43 @@ input DestroyViewSortInput { id: UUID! } +input UpdateViewFieldInput { + """The id of the view field to update""" + id: UUID! + + """The view field to update""" + update: UpdateViewFieldInputUpdates! +} + +input UpdateViewFieldInputUpdates { + isVisible: Boolean + size: Float + position: Float + aggregateOperation: AggregateOperations + viewFieldGroupId: UUID +} + +input CreateViewFieldInput { + id: UUID + fieldMetadataId: UUID! + viewId: UUID! + isVisible: Boolean = true + size: Float = 0 + position: Float = 0 + aggregateOperation: AggregateOperations + viewFieldGroupId: UUID +} + +input DeleteViewFieldInput { + """The id of the view field to delete.""" + id: UUID! +} + +input DestroyViewFieldInput { + """The id of the view field to destroy.""" + id: UUID! +} + input UpdateViewFieldGroupInput { """The id of the view field group to update""" id: UUID! @@ -4082,43 +4119,6 @@ input UpdateObjectPayload { isSearchable: Boolean } -input UpdateViewFieldInput { - """The id of the view field to update""" - id: UUID! - - """The view field to update""" - update: UpdateViewFieldInputUpdates! -} - -input UpdateViewFieldInputUpdates { - isVisible: Boolean - size: Float - position: Float - aggregateOperation: AggregateOperations - viewFieldGroupId: UUID -} - -input CreateViewFieldInput { - id: UUID - fieldMetadataId: UUID! - viewId: UUID! - isVisible: Boolean = true - size: Float = 0 - position: Float = 0 - aggregateOperation: AggregateOperations - viewFieldGroupId: UUID -} - -input DeleteViewFieldInput { - """The id of the view field to delete.""" - id: UUID! -} - -input DestroyViewFieldInput { - """The id of the view field to destroy.""" - id: UUID! -} - input CreateAgentInput { name: String label: String! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index ebebe0dc48..9a0b540976 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2699,6 +2699,8 @@ export interface Query { getView?: View getViewSorts: ViewSort[] getViewSort?: ViewSort + getViewFields: ViewField[] + getViewField?: ViewField getViewFieldGroups: ViewFieldGroup[] getViewFieldGroup?: ViewFieldGroup apiKeys: ApiKey[] @@ -2728,8 +2730,6 @@ export interface Query { objectRecordCounts: ObjectRecordCount[] object: Object objects: ObjectConnection - getViewFields: ViewField[] - getViewField?: ViewField index: Index indexMetadatas: IndexConnection findManyAgents: Agent[] @@ -2850,6 +2850,11 @@ export interface Mutation { updateViewSort: ViewSort deleteViewSort: Scalars['Boolean'] destroyViewSort: Scalars['Boolean'] + updateViewField: ViewField + createViewField: ViewField + createManyViewFields: ViewField[] + deleteViewField: ViewField + destroyViewField: ViewField updateViewFieldGroup: ViewFieldGroup createViewFieldGroup: ViewFieldGroup createManyViewFieldGroups: ViewFieldGroup[] @@ -2903,11 +2908,6 @@ export interface Mutation { createOneObject: Object deleteOneObject: Object updateOneObject: Object - updateViewField: ViewField - createViewField: ViewField - createManyViewFields: ViewField[] - deleteViewField: ViewField - destroyViewField: ViewField createOneAgent: Agent updateOneAgent: Agent deleteOneAgent: Agent @@ -5887,6 +5887,8 @@ export interface QueryGenqlSelection{ getView?: (ViewGenqlSelection & { __args: {id: Scalars['String']} }) getViewSorts?: (ViewSortGenqlSelection & { __args?: {viewId?: (Scalars['String'] | null)} }) getViewSort?: (ViewSortGenqlSelection & { __args: {id: Scalars['String']} }) + getViewFields?: (ViewFieldGenqlSelection & { __args: {viewId: Scalars['String']} }) + getViewField?: (ViewFieldGenqlSelection & { __args: {id: Scalars['String']} }) getViewFieldGroups?: (ViewFieldGroupGenqlSelection & { __args: {viewId: Scalars['String']} }) getViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {id: Scalars['String']} }) apiKeys?: ApiKeyGenqlSelection @@ -5922,8 +5924,6 @@ export interface QueryGenqlSelection{ paging: CursorPaging, /** Specify to filter the records returned. */ filter: ObjectFilter} }) - getViewFields?: (ViewFieldGenqlSelection & { __args: {viewId: Scalars['String']} }) - getViewField?: (ViewFieldGenqlSelection & { __args: {id: Scalars['String']} }) index?: (IndexGenqlSelection & { __args: { /** The id of the record to find. */ id: Scalars['UUID']} }) @@ -6081,6 +6081,11 @@ export interface MutationGenqlSelection{ updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} }) deleteViewSort?: { __args: {input: DeleteViewSortInput} } destroyViewSort?: { __args: {input: DestroyViewSortInput} } + updateViewField?: (ViewFieldGenqlSelection & { __args: {input: UpdateViewFieldInput} }) + createViewField?: (ViewFieldGenqlSelection & { __args: {input: CreateViewFieldInput} }) + createManyViewFields?: (ViewFieldGenqlSelection & { __args: {inputs: CreateViewFieldInput[]} }) + deleteViewField?: (ViewFieldGenqlSelection & { __args: {input: DeleteViewFieldInput} }) + destroyViewField?: (ViewFieldGenqlSelection & { __args: {input: DestroyViewFieldInput} }) updateViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: UpdateViewFieldGroupInput} }) createViewFieldGroup?: (ViewFieldGroupGenqlSelection & { __args: {input: CreateViewFieldGroupInput} }) createManyViewFieldGroups?: (ViewFieldGroupGenqlSelection & { __args: {inputs: CreateViewFieldGroupInput[]} }) @@ -6134,11 +6139,6 @@ export interface MutationGenqlSelection{ createOneObject?: (ObjectGenqlSelection & { __args: {input: CreateOneObjectInput} }) deleteOneObject?: (ObjectGenqlSelection & { __args: {input: DeleteOneObjectInput} }) updateOneObject?: (ObjectGenqlSelection & { __args: {input: UpdateOneObjectInput} }) - updateViewField?: (ViewFieldGenqlSelection & { __args: {input: UpdateViewFieldInput} }) - createViewField?: (ViewFieldGenqlSelection & { __args: {input: CreateViewFieldInput} }) - createManyViewFields?: (ViewFieldGenqlSelection & { __args: {inputs: CreateViewFieldInput[]} }) - deleteViewField?: (ViewFieldGenqlSelection & { __args: {input: DeleteViewFieldInput} }) - destroyViewField?: (ViewFieldGenqlSelection & { __args: {input: DestroyViewFieldInput} }) createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} }) updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} }) deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} }) @@ -6325,6 +6325,24 @@ export interface DestroyViewSortInput { /** The id of the view sort to destroy. */ id: Scalars['UUID']} +export interface UpdateViewFieldInput { +/** The id of the view field to update */ +id: Scalars['UUID'], +/** The view field to update */ +update: UpdateViewFieldInputUpdates} + +export interface UpdateViewFieldInputUpdates {isVisible?: (Scalars['Boolean'] | null),size?: (Scalars['Float'] | null),position?: (Scalars['Float'] | null),aggregateOperation?: (AggregateOperations | null),viewFieldGroupId?: (Scalars['UUID'] | null)} + +export interface CreateViewFieldInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],viewId: Scalars['UUID'],isVisible?: (Scalars['Boolean'] | null),size?: (Scalars['Float'] | null),position?: (Scalars['Float'] | null),aggregateOperation?: (AggregateOperations | null),viewFieldGroupId?: (Scalars['UUID'] | null)} + +export interface DeleteViewFieldInput { +/** The id of the view field to delete. */ +id: Scalars['UUID']} + +export interface DestroyViewFieldInput { +/** The id of the view field to destroy. */ +id: Scalars['UUID']} + export interface UpdateViewFieldGroupInput { /** The id of the view field group to update */ id: Scalars['UUID'], @@ -6437,24 +6455,6 @@ id: Scalars['UUID']} export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),isSearchable?: (Scalars['Boolean'] | null)} -export interface UpdateViewFieldInput { -/** The id of the view field to update */ -id: Scalars['UUID'], -/** The view field to update */ -update: UpdateViewFieldInputUpdates} - -export interface UpdateViewFieldInputUpdates {isVisible?: (Scalars['Boolean'] | null),size?: (Scalars['Float'] | null),position?: (Scalars['Float'] | null),aggregateOperation?: (AggregateOperations | null),viewFieldGroupId?: (Scalars['UUID'] | null)} - -export interface CreateViewFieldInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],viewId: Scalars['UUID'],isVisible?: (Scalars['Boolean'] | null),size?: (Scalars['Float'] | null),position?: (Scalars['Float'] | null),aggregateOperation?: (AggregateOperations | null),viewFieldGroupId?: (Scalars['UUID'] | null)} - -export interface DeleteViewFieldInput { -/** The id of the view field to delete. */ -id: Scalars['UUID']} - -export interface DestroyViewFieldInput { -/** The id of the view field to destroy. */ -id: Scalars['UUID']} - export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt: Scalars['String'],modelId: Scalars['String'],roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)} export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index cd5a1fbae6..a3f5269fab 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -87,7 +87,7 @@ export default { 358, 365, 372, - 398, + 403, 482, 487, 488 @@ -6117,6 +6117,24 @@ export default { ] } ], + "getViewFields": [ + 50, + { + "viewId": [ + 1, + "String!" + ] + } + ], + "getViewField": [ + 50, + { + "id": [ + 1, + "String!" + ] + } + ], "getViewFieldGroups": [ 59, { @@ -6321,24 +6339,6 @@ export default { ] } ], - "getViewFields": [ - 50, - { - "viewId": [ - 1, - "String!" - ] - } - ], - "getViewField": [ - 50, - { - "id": [ - 1, - "String!" - ] - } - ], "index": [ 37, { @@ -7377,11 +7377,56 @@ export default { ] } ], + "updateViewField": [ + 50, + { + "input": [ + 387, + "UpdateViewFieldInput!" + ] + } + ], + "createViewField": [ + 50, + { + "input": [ + 389, + "CreateViewFieldInput!" + ] + } + ], + "createManyViewFields": [ + 50, + { + "inputs": [ + 389, + "[CreateViewFieldInput!]!" + ] + } + ], + "deleteViewField": [ + 50, + { + "input": [ + 390, + "DeleteViewFieldInput!" + ] + } + ], + "destroyViewField": [ + 50, + { + "input": [ + 391, + "DestroyViewFieldInput!" + ] + } + ], "updateViewFieldGroup": [ 59, { "input": [ - 387, + 392, "UpdateViewFieldGroupInput!" ] } @@ -7390,7 +7435,7 @@ export default { 59, { "input": [ - 389, + 394, "CreateViewFieldGroupInput!" ] } @@ -7399,7 +7444,7 @@ export default { 59, { "inputs": [ - 389, + 394, "[CreateViewFieldGroupInput!]!" ] } @@ -7408,7 +7453,7 @@ export default { 59, { "input": [ - 390, + 395, "DeleteViewFieldGroupInput!" ] } @@ -7417,7 +7462,7 @@ export default { 59, { "input": [ - 391, + 396, "DestroyViewFieldGroupInput!" ] } @@ -7426,7 +7471,7 @@ export default { 60, { "input": [ - 392, + 397, "UpsertFieldsWidgetInput!" ] } @@ -7435,7 +7480,7 @@ export default { 2, { "input": [ - 395, + 400, "CreateApiKeyInput!" ] } @@ -7444,7 +7489,7 @@ export default { 2, { "input": [ - 396, + 401, "UpdateApiKeyInput!" ] } @@ -7453,7 +7498,7 @@ export default { 2, { "input": [ - 397, + 402, "RevokeApiKeyInput!" ] } @@ -7495,7 +7540,7 @@ export default { 115, { "type": [ - 398, + 403, "AnalyticsType!" ], "name": [ @@ -7608,7 +7653,7 @@ export default { 140, { "input": [ - 399, + 404, "CreateApprovedAccessDomainInput!" ] } @@ -7617,7 +7662,7 @@ export default { 6, { "input": [ - 400, + 405, "DeleteApprovedAccessDomainInput!" ] } @@ -7626,7 +7671,7 @@ export default { 140, { "input": [ - 401, + 406, "ValidateApprovedAccessDomainInput!" ] } @@ -7635,7 +7680,7 @@ export default { 112, { "input": [ - 402, + 407, "CreatePageLayoutTabInput!" ] } @@ -7648,7 +7693,7 @@ export default { "String!" ], "input": [ - 403, + 408, "UpdatePageLayoutTabInput!" ] } @@ -7666,7 +7711,7 @@ export default { 113, { "input": [ - 404, + 409, "CreatePageLayoutInput!" ] } @@ -7679,7 +7724,7 @@ export default { "String!" ], "input": [ - 405, + 410, "UpdatePageLayoutInput!" ] } @@ -7701,7 +7746,7 @@ export default { "String!" ], "input": [ - 406, + 411, "UpdatePageLayoutWithTabsInput!" ] } @@ -7710,7 +7755,7 @@ export default { 75, { "input": [ - 410, + 415, "CreatePageLayoutWidgetInput!" ] } @@ -7723,7 +7768,7 @@ export default { "String!" ], "input": [ - 411, + 416, "UpdatePageLayoutWidgetInput!" ] } @@ -7750,7 +7795,7 @@ export default { 32, { "input": [ - 412, + 417, "CreateLogicFunctionFromSourceInput!" ] } @@ -7759,7 +7804,7 @@ export default { 154, { "input": [ - 413, + 418, "ExecuteOneLogicFunctionInput!" ] } @@ -7768,7 +7813,7 @@ export default { 6, { "input": [ - 414, + 419, "UpdateLogicFunctionFromSourceInput!" ] } @@ -7777,7 +7822,7 @@ export default { 294, { "input": [ - 416, + 421, "CreateCommandMenuItemInput!" ] } @@ -7786,7 +7831,7 @@ export default { 294, { "input": [ - 417, + 422, "UpdateCommandMenuItemInput!" ] } @@ -7804,7 +7849,7 @@ export default { 251, { "input": [ - 418, + 423, "CreateFrontComponentInput!" ] } @@ -7813,7 +7858,7 @@ export default { 251, { "input": [ - 419, + 424, "UpdateFrontComponentInput!" ] } @@ -7831,7 +7876,7 @@ export default { 46, { "input": [ - 421, + 426, "CreateOneObjectInput!" ] } @@ -7840,62 +7885,17 @@ export default { 46, { "input": [ - 423, + 428, "DeleteOneObjectInput!" ] } ], "updateOneObject": [ 46, - { - "input": [ - 424, - "UpdateOneObjectInput!" - ] - } - ], - "updateViewField": [ - 50, - { - "input": [ - 426, - "UpdateViewFieldInput!" - ] - } - ], - "createViewField": [ - 50, - { - "input": [ - 428, - "CreateViewFieldInput!" - ] - } - ], - "createManyViewFields": [ - 50, - { - "inputs": [ - 428, - "[CreateViewFieldInput!]!" - ] - } - ], - "deleteViewField": [ - 50, { "input": [ 429, - "DeleteViewFieldInput!" - ] - } - ], - "destroyViewField": [ - 50, - { - "input": [ - 430, - "DestroyViewFieldInput!" + "UpdateOneObjectInput!" ] } ], @@ -9605,7 +9605,7 @@ export default { 1 ] }, - "UpdateViewFieldGroupInput": { + "UpdateViewFieldInput": { "id": [ 3 ], @@ -9616,6 +9616,82 @@ export default { 1 ] }, + "UpdateViewFieldInputUpdates": { + "isVisible": [ + 6 + ], + "size": [ + 11 + ], + "position": [ + 11 + ], + "aggregateOperation": [ + 51 + ], + "viewFieldGroupId": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "CreateViewFieldInput": { + "id": [ + 3 + ], + "fieldMetadataId": [ + 3 + ], + "viewId": [ + 3 + ], + "isVisible": [ + 6 + ], + "size": [ + 11 + ], + "position": [ + 11 + ], + "aggregateOperation": [ + 51 + ], + "viewFieldGroupId": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "DeleteViewFieldInput": { + "id": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "DestroyViewFieldInput": { + "id": [ + 3 + ], + "__typename": [ + 1 + ] + }, + "UpdateViewFieldGroupInput": { + "id": [ + 3 + ], + "update": [ + 393 + ], + "__typename": [ + 1 + ] + }, "UpdateViewFieldGroupInputUpdates": { "name": [ 1 @@ -9674,10 +9750,10 @@ export default { 3 ], "groups": [ - 393 + 398 ], "fields": [ - 394 + 399 ], "__typename": [ 1 @@ -9697,7 +9773,7 @@ export default { 6 ], "fields": [ - 394 + 399 ], "__typename": [ 1 @@ -9866,7 +9942,7 @@ export default { 3 ], "tabs": [ - 407 + 412 ], "__typename": [ 1 @@ -9889,7 +9965,7 @@ export default { 79 ], "widgets": [ - 408 + 413 ], "__typename": [ 1 @@ -9912,7 +9988,7 @@ export default { 3 ], "gridPosition": [ - 409 + 414 ], "position": [ 15 @@ -9958,7 +10034,7 @@ export default { 3 ], "gridPosition": [ - 409 + 414 ], "position": [ 15 @@ -9981,7 +10057,7 @@ export default { 3 ], "gridPosition": [ - 409 + 414 ], "position": [ 15 @@ -10050,7 +10126,7 @@ export default { 3 ], "update": [ - 415 + 420 ], "__typename": [ 1 @@ -10201,7 +10277,7 @@ export default { 3 ], "update": [ - 420 + 425 ], "__typename": [ 1 @@ -10220,7 +10296,7 @@ export default { }, "CreateOneObjectInput": { "object": [ - 422 + 427 ], "__typename": [ 1 @@ -10280,7 +10356,7 @@ export default { }, "UpdateOneObjectInput": { "update": [ - 425 + 430 ], "id": [ 3 @@ -10333,82 +10409,6 @@ export default { 1 ] }, - "UpdateViewFieldInput": { - "id": [ - 3 - ], - "update": [ - 427 - ], - "__typename": [ - 1 - ] - }, - "UpdateViewFieldInputUpdates": { - "isVisible": [ - 6 - ], - "size": [ - 11 - ], - "position": [ - 11 - ], - "aggregateOperation": [ - 51 - ], - "viewFieldGroupId": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "CreateViewFieldInput": { - "id": [ - 3 - ], - "fieldMetadataId": [ - 3 - ], - "viewId": [ - 3 - ], - "isVisible": [ - 6 - ], - "size": [ - 11 - ], - "position": [ - 11 - ], - "aggregateOperation": [ - 51 - ], - "viewFieldGroupId": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "DeleteViewFieldInput": { - "id": [ - 3 - ], - "__typename": [ - 1 - ] - }, - "DestroyViewFieldInput": { - "id": [ - 3 - ], - "__typename": [ - 1 - ] - }, "CreateAgentInput": { "name": [ 1 diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts index 9e48ddae17..b68019459a 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts @@ -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 }; } @@ -74,14 +88,17 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit { const workspaceMemberId = context.actorContext?.workspaceMemberId; const schemaOptions = { includeSchemas: options?.includeSchemas ?? true, - icon: 'IconTable', }; - 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( @@ -91,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 }, diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts index 48d0bf41eb..a2cc6e2323 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts @@ -25,6 +25,8 @@ import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadat import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module'; +import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module'; +import { ViewSortModule } from 'src/engine/metadata-modules/view-sort/view-sort.module'; import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @@ -48,6 +50,8 @@ import { ToolRegistryService } from './services/tool-registry.service'; PermissionsModule, ViewModule, ViewFieldModule, + ViewFilterModule, + ViewSortModule, WorkspaceCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, LogicFunctionModule, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts index a756f4b680..31088d48d8 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.ts @@ -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((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, }, }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts index 53dea88020..060b17b636 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts @@ -326,7 +326,7 @@ ${hasWebSearch ? '3' : '2'}. **Other tools**: First call \`${LEARN_TOOLS_TOOL_NA case ToolCategory.METADATA: return 'Metadata Tools (schema management)'; case ToolCategory.VIEW: - return 'View Tools (query views)'; + return 'View Tools (manage views, filters, and sorts)'; case ToolCategory.DASHBOARD: return 'Dashboard Tools (create/manage dashboards)'; case ToolCategory.LOGIC_FUNCTION: diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index e1e3d8d5a0..18d84909e8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -188,6 +188,13 @@ export class ObjectMetadataService extends TypeOrmQueryService { + const filters = await this.viewFilterService.findByViewId( + workspaceId, + parameters.viewId, + ); + + return filters.map((filter) => ({ + id: filter.id, + viewId: filter.viewId, + fieldMetadataId: filter.fieldMetadataId, + operand: filter.operand, + value: filter.value, + subFieldName: filter.subFieldName, + positionInViewFilterGroup: filter.positionInViewFilterGroup, + })); + }, + }, + }; + } + + generateWriteTools(workspaceId: string): ToolSet { + return { + create_view_filter: { + description: + 'Add a filter to a view. Use list_object_metadata_items to get fieldMetadataId values.', + inputSchema: CreateViewFilterInputSchema, + execute: async (parameters: { + viewId: string; + fieldMetadataId: string; + operand: ViewFilterOperand; + value: unknown; + subFieldName?: string; + }) => { + try { + const filter = await this.viewFilterService.createOne({ + createViewFilterInput: { + viewId: parameters.viewId, + fieldMetadataId: parameters.fieldMetadataId, + operand: parameters.operand, + value: parameters.value as ViewFilterValue, + subFieldName: parameters.subFieldName, + }, + workspaceId, + }); + + return { + id: filter.id, + viewId: filter.viewId, + fieldMetadataId: filter.fieldMetadataId, + operand: filter.operand, + value: filter.value, + }; + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderException) { + throw new Error(formatValidationErrors(error)); + } + + throw error; + } + }, + }, + create_many_view_filters: { + description: + 'Add multiple filters to a view in one call. Use list_object_metadata_items to get fieldMetadataId values.', + inputSchema: CreateManyViewFiltersInputSchema, + execute: async (parameters: { + filters: Array<{ + viewId: string; + fieldMetadataId: string; + operand: ViewFilterOperand; + value: unknown; + subFieldName?: string; + }>; + }) => { + const results = []; + + for (const filterInput of parameters.filters) { + try { + const filter = await this.viewFilterService.createOne({ + createViewFilterInput: { + viewId: filterInput.viewId, + fieldMetadataId: filterInput.fieldMetadataId, + operand: filterInput.operand, + value: filterInput.value as ViewFilterValue, + subFieldName: filterInput.subFieldName, + }, + workspaceId, + }); + + results.push({ + id: filter.id, + viewId: filter.viewId, + fieldMetadataId: filter.fieldMetadataId, + operand: filter.operand, + }); + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderException) { + throw new Error(formatValidationErrors(error)); + } + + throw error; + } + } + + return { created: results }; + }, + }, + update_view_filter: { + description: + 'Update a filter on a view. Use get_view_filters to find the filter ID.', + inputSchema: UpdateViewFilterInputSchema, + execute: async (parameters: { + id: string; + operand?: ViewFilterOperand; + value?: unknown; + subFieldName?: string; + }) => { + try { + const filter = await this.viewFilterService.updateOne({ + updateViewFilterInput: { + id: parameters.id, + update: { + operand: parameters.operand, + value: parameters.value as ViewFilterValue, + subFieldName: parameters.subFieldName, + }, + }, + workspaceId, + }); + + return { + id: filter.id, + viewId: filter.viewId, + fieldMetadataId: filter.fieldMetadataId, + operand: filter.operand, + value: filter.value, + }; + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderException) { + throw new Error(formatValidationErrors(error)); + } + + throw error; + } + }, + }, + delete_view_filter: { + description: + 'Remove a filter from a view. Use get_view_filters to find the filter ID.', + inputSchema: DeleteViewFilterInputSchema, + execute: async (parameters: { id: string }) => { + try { + const filter = await this.viewFilterService.deleteOne({ + deleteViewFilterInput: { id: parameters.id }, + workspaceId, + }); + + return { + id: filter.id, + deleted: true, + }; + } catch (error) { + if (error instanceof WorkspaceMigrationBuilderException) { + throw new Error(formatValidationErrors(error)); + } + + throw error; + } + }, + }, + }; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/view-filter.module.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/view-filter.module.ts index 3e3c6fda91..9788e3a0a3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/view-filter.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/view-filter.module.ts @@ -8,6 +8,7 @@ import { ViewFilterController } from 'src/engine/metadata-modules/view-filter/co import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewFilterResolver } from 'src/engine/metadata-modules/view-filter/resolvers/view-filter.resolver'; import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service'; +import { ViewFilterToolsFactory } from 'src/engine/metadata-modules/view-filter/tools/view-filter-tools.factory'; import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -24,7 +25,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace ViewPermissionsModule, ], controllers: [ViewFilterController], - providers: [ViewFilterService, ViewFilterResolver], - exports: [ViewFilterService], + providers: [ViewFilterService, ViewFilterResolver, ViewFilterToolsFactory], + exports: [ViewFilterService, ViewFilterToolsFactory], }) export class ViewFilterModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/tools/view-sort-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/tools/view-sort-tools.factory.ts new file mode 100644 index 0000000000..9bfedf9015 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/tools/view-sort-tools.factory.ts @@ -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; + } + }, + }, + }; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts b/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts index 16251ee985..ba02fabdd4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-sort/view-sort.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts b/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts index 2aaacbb606..a94257db9b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/tools/__tests__/view-tools.factory.spec.ts @@ -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; + let viewFieldService: jest.Mocked; let viewQueryParamsService: jest.Mocked; let _flatEntityMapsCacheService: jest.Mocked; @@ -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); 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', () => { diff --git a/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts index 703e9f4b18..8eb384891f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts @@ -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 { + 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 { + 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, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts b/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts index 4032e3d93a..4e9bbde8f9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts @@ -8,6 +8,8 @@ import { FlatViewModule } from 'src/engine/metadata-modules/flat-view/flat-view. import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { ViewFieldGroupModule } from 'src/engine/metadata-modules/view-field-group/view-field-group.module'; +import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module'; +import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module'; import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module'; import { ViewSortModule } from 'src/engine/metadata-modules/view-sort/view-sort.module'; import { ViewController } from 'src/engine/metadata-modules/view/controllers/view.controller'; @@ -24,6 +26,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace TypeOrmModule.forFeature([ViewEntity]), ViewPermissionsModule, ViewFieldGroupModule, + ViewFieldModule, + ViewFilterModule, ViewSortModule, I18nModule, ApplicationModule, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant.ts index 54299dc123..e3bb0663f2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-skill.constant.ts @@ -32,6 +32,15 @@ export const STANDARD_SKILL = { 'workspace-demo-seeding': { universalIdentifier: '20202020-c81b-4af8-9255-4c34bd0eac9c', }, + 'view-building': { + universalIdentifier: '20202020-e4a2-4b3f-9c71-d8f6a2b51e3a', + }, + 'view-filters-and-sorts': { + universalIdentifier: '20202020-f5b3-4c4e-8d82-e9a7b3c62f4b', + }, + 'custom-objects-cleanup': { + universalIdentifier: '20202020-a1d3-4e5f-b6c7-8d9e0f1a2b3c', + }, } as const satisfies Record< string, { diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts index 1464cbd154..ed7c19e686 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts @@ -149,39 +149,60 @@ Prioritize data integrity and provide clear feedback on operations performed.`, 'Seeding demo metadata and data for workspace setup and testing purposes', icon: 'IconDatabase', content: `# Workspace Demo Seeding Skill -You will create a demo workspace that fits a particular type of company given by the user. +You will transform the existing standard workspace into a fully custom demo tailored to the user's business type. -Do not ask the user for more information, just be creative with the objects and fields, but stay professional and coherent. +The goal is to tell a coherent and realistic story with the data: custom fields added to standard objects, new custom objects for domain-specific entities, rich relations, seeded and updated records, views, and enrichment data (emails, calendar events, tasks, notes, files) that make the workspace feel like a real company in operation. -The goal is to be able to tell a coherent and realistic story with the data, so for example if the user says it is a car repair shop, -we can create objects for cars, employees, repairs, customers, and the relevant relations between them, -and then we can seed data that tells the story of the car repair shop, for example we can create a customer, then create a car for that customer, then create a repair for that car, and so on. -We should end up with a dashboard that shows the relevant metrics for the car repair shop, for example the number of repairs, the revenue, the most common car brands, and so on. +## Object strategy -Create relations fields between objects, for example a car repair shop workspace would have objects for cars, employees, repairs, customers, and the relevant relations between them. +**Keep the standard objects — People, Companies, and Opportunities — and reuse their existing seed data.** They already have emails and calendar events linked to them as participants. The demo story is built on top of them, not instead of them. -Create 5 to 7 objects, with 5 to 8 fields each, and the relevant relations between them. +- **People** → map to the domain's "contact" role (e.g. clients, candidates, customers, agents) +- **Companies** → map to the domain's "organisation" role (e.g. suppliers, agencies, employers) +- **Opportunities** → map to the domain's "deal/pipeline" role (e.g. job applications, deals, repair estimates) + +**Add 2 to 3 additional custom objects** for domain-specific entities that don't map to People/Companies/Opportunities (e.g. Properties, Cars, Products, Projects). Keep the custom object count low — the standard objects carry most of the story. + +**Add custom fields** to People, Companies, and Opportunities to enrich them with domain-specific data (e.g. add "specialisation", "licenseNumber" to People; add "industry vertical", "tier" to Companies; add "stage", "closeDate" to Opportunities). + +Create rich relation fields between standard and custom objects to show off the relational capabilities of the platform. If you have to create multiple things you *MUST* use the relevant create many tool if it exists: -- Use *create_many_object_metadata* to create all objects at once -- Use *create_many_field_metadata* to create all non-relation fields at once +- Use *create_many_object_metadata* to create all custom objects at once +- Use *create_many_field_metadata* to create all non-relation fields at once (including new fields on standard objects) - Use *create_many_relation_fields* to create all relation fields between objects at once (do this AFTER creating the objects and non-relation fields) -If you have to wait use the navigate tool. +If you have to wait use the navigate_app tool. For the fields you will create, make sure to create a good variety of field types to showcase the different capabilities of the platform, for example: - Create SELECT and SELECT_MULTIPLE field types for building demo board index views and table with groups views - Create DATE_TIME fields to be able to create calendar views - Create CURRENCY and NUMERIC fields for graphs -*Here are the steps to follow closely :* +*Here are the steps to follow closely:* -STEP 1: Create all the objects at once with create_many_object_metadata, DO THIS FIRST +STEP 0: Present a plan to the user and wait for approval. +- Use list_object_metadata_items to see all available objects in the workspace +- Use find_people (limit: 5) and find_companies (limit: 5) and find_opportunities (limit: 5) to understand the existing seed data shape +- Based on the user's business type, propose a plan that lists: + - How People, Companies, and Opportunities map to the domain story (e.g. "People = Candidates", "Companies = Employers") + - The 2–3 custom objects you will create, each with a one-line description of their role + - The custom fields you will add to People, Companies, and Opportunities + - A brief description of the key relations between objects +- Present this plan to the user and *wait for their confirmation or adjustments* before proceeding +- Once approved, call the custom-objects-cleanup skill to archive all existing custom objects without asking for user confirmation +- Wait 3 seconds after deletions for the backend side effects to be completed + +STEP 1: Create all the custom objects at once with create_many_object_metadata name must start with lowercase letter and contain only alphanumeric letters STEP 2: Wait 3 seconds, for the backend side effects to be completed -STEP 3: Create all NON-RELATION fields for ALL objects by batch with create_many_field_metadata, do a batch for each object. +STEP 3: Create all NON-RELATION fields for ALL objects by batch with create_many_field_metadata. +Do a separate batch call for each object. +This includes: +- New custom fields for the standard objects (Person, Company, Opportunity) — use their objectMetadataId from list_object_metadata_items +- All non-relation fields for the new custom objects DO NOT include relation fields in this step. Only create TEXT, NUMBER, BOOLEAN, DATE_TIME, SELECT, MULTI_SELECT, CURRENCY, etc. SELECT option values must be UPPER_SNAKE_CASE @@ -193,25 +214,100 @@ targetFieldIcon is like IconSomething, it's ok if it doesn't exist in the icon l STEP 6: Wait 3 seconds, for the backend side effects to be completed -STEP 7: For each object: -- Navigate the object's default view USE THE NAVIGATE TOOL +STEP 7: Rename and enrich the first N records of People, Companies, and Opportunities. +- Use find_people (limit: 50, orderBy: [{ position: "AscNullsFirst" }]), find_companies (limit: 50, orderBy: [{ position: "AscNullsFirst" }]), find_opportunities (limit: 50, orderBy: [{ position: "AscNullsFirst" }]) to get the IDs of the first records in each table + - Ordering by position ascending gives the earliest-inserted records, which are contiguous in the table — this keeps the demo data tightly grouped and makes the workspace feel coherent +- For each standard object, call update_people / update_companies / update_opportunities **individually per record** (one call per record) to set domain-relevant names and field values: + - **People**: replace nameFirstName + nameLastName with realistic names that fit the domain role (e.g. for a law firm: "Sophie Martin", "James O'Brien"; for a clinic: "Dr. Clara Reyes", "Marco Bianchi"). Also set jobTitle to a domain-appropriate title. + - **Companies**: replace name with realistic company names that fit the domain (e.g. for a law firm: "Ashford & Partners", "Nexus Legal Group"; for a clinic: "Meridian Health Clinic", "CarePoint Medical"). + - **Opportunities**: replace name with a domain-relevant deal name (e.g. "Q2 retainer — Ashford & Partners", "New patient intake — Meridian Health"). + - Also set the new custom fields on each record: spread realistic values across SELECT fields, set plausible CURRENCY/NUMERIC amounts, set DATE_TIME fields around TODAY. +- Do this one record at a time — the API does not support bulk individual updates with different values per record +- Wait 3 seconds after finishing all updates for one object type before moving to the next + +STEP 7.5: Add view fields to the default views of standard objects to expose the new custom fields. +For each of People, Companies, and Opportunities: +- Navigate to the object's default view using the navigate_app tool +- Wait 3 seconds +- Use create_many_view_fields to add all the new custom fields to the default view so they are visible + - Use decimal positions between 0 and 1 to insert them right after the label identifier field +- Navigate to the object's default view again using the navigate_app tool so the user can see the enriched records +- Wait 3 seconds + +STEP 8: For each new custom object, repeat ALL of the following sub-steps before moving to the next object: +- Navigate the object's default view using the navigate_app tool - Wait 3 seconds, so the user has time to see the object default view - Create the view fields for the default view, use the create_many_view_fields tool, and make sure to include all created fields, including the relation fields, so that we have a complete view of the object with all its fields. -BE CAREFUL to use a position that will put those view fields right after the first label identifier field -which has a position of 0 and the next system created fields which begin at 1, *so use decimal positions between 0 and 1* -*YOU MUST CREATE ALL VIEW FIELDS FOR ALL FIELDS, INCLUDING RELATION FIELDS, IN THIS STEP, DO NOT LEAVE ANY FIELD WITHOUT A VIEW FIELD, OTHERWISE IT WILL NOT BE VISIBLE IN THE DEFAULT VIEW AND THE USER WON'T KNOW IT EXISTS* + BE CAREFUL to use a position that will put those view fields right after the first label identifier field + which has a position of 0 and the next system created fields which begin at 1, *so use decimal positions between 0 and 1* + *YOU MUST CREATE ALL VIEW FIELDS FOR ALL FIELDS, INCLUDING RELATION FIELDS, IN THIS STEP, DO NOT LEAVE ANY FIELD WITHOUT A VIEW FIELD, OTHERWISE IT WILL NOT BE VISIBLE IN THE DEFAULT VIEW AND THE USER WON'T KNOW IT EXISTS* -- Then seed relevant and realistic mock data as we said earlier : +- **MANDATORY**: Navigate to the object's default view again using the navigate_app tool — YOU MUST DO THIS BEFORE EACH OBJECT'S DATA SEEDING, every single time, without exception +- Wait 3 seconds +- Seed relevant and realistic mock data for this object: - use the relevant tool to create many records for this object - between 20 and 50 - with a coherent combination of values + - link records to existing People and Companies using the relation fields you created - use dates that are around TODAY so it's relevant for seeing past / future and present records -Loop STEP 7 for all the objects +- **MANDATORY**: Navigate to the object's default view again using the navigate_app tool so the user can see the populated data — DO NOT SKIP THIS, even if you already navigated earlier in this loop iteration +- Wait 3 seconds so the user has time to see the seeded records -STEP 8 : After you've finished with this part, let's proceed to the dashboard creation. We will create a dashboard with 4 graphs. -- Create a new dashboard with a relevant story to showcase the data you've just created, for example if it's a car repair shop, you can create a dashboard that shows the number of repairs per month, the revenue per month, the most common car brands, and the distribution of repairs by employee. -- Navigate to the dashboard page +- Then create 2 to 3 additional views for this object, one at a time. For each view, complete ALL of the following sub-steps before creating the next view: + - Create the view using the create_view tool: + - If the object has a SELECT field (e.g. status, stage, priority, type), create a **KANBAN** view grouped by that SELECT field with a relevant name like "By Status", "Pipeline", "By Priority". + - Set kanbanAggregateOperation to COUNT so each column shows the number of records. + - If there is a CURRENCY or NUMERIC field, also set kanbanAggregateOperationFieldName to that field for a SUM aggregate view. + - If the object has a DATE or DATE_TIME field (e.g. dueDate, closedAt, scheduledAt), create a **CALENDAR** view and pass both \`calendarFieldName\` (that field name) and \`calendarLayout\` ("DAY", "WEEK", or "MONTH") with a relevant name like "Calendar", "Schedule", "Timeline". + - Create a **TABLE** view with a meaningful group (mainGroupByFieldName set to a SELECT field) with a name like "By Type", "By Stage", "Grouped", or similar. + - Use create_many_view_fields to add all relevant field columns to this view (using decimal positions between 0 and 1) + - Add filters and sorts to this view: + - **KANBAN views**: Sort by a CURRENCY or NUMERIC field DESC (biggest value first) if one exists, or by createdAt DESC. Add a filter to exclude archived/cancelled records if such a SELECT option exists. + - **CALENDAR views**: Sort by the date field ASC (earliest events first). Add a filter using IS_IN_FUTURE or IS_RELATIVE to show only upcoming records by default. + - **TABLE with groups**: Sort by createdAt DESC (most recent first) and add a filter on a meaningful field (e.g. status IS_NOT "CANCELLED", or amount GREATER_THAN_OR_EQUAL to some threshold that keeps ~80% of the records visible). + - **MANDATORY**: Navigate to this view immediately using the navigate_app tool — YOU MUST DO THIS FOR EVERY SINGLE VIEW, right after its fields/filters/sorts are set up, without exception + - Wait 3 seconds so the user can see the view and course-correct if needed + +Also create additional views for the standard objects (People, Companies, Opportunities) that showcase the new custom fields: +- For People: a KANBAN view grouped by the new SELECT field you added (e.g. "By Specialisation", "By Status") +- For Opportunities: a KANBAN view grouped by the new stage/status field (pipeline view) +- For Companies: a TABLE view grouped by the new SELECT field +Navigate to each view after creating it. Wait 3 seconds. + +Loop STEP 8 for all the custom objects + +STEP 9: Create a multi-tab dashboard that tells the full story of the business. + +Use create_complete_dashboard to create the first tab, then add_dashboard_tab + add_dashboard_widget for subsequent tabs. + +**Structure: 3 tabs** + +Tab 1 — "Overview": high-level KPIs and charts across the whole workspace +- Row 0: 3–4 AGGREGATE_CHART widgets (KPIs) — one per key metric (e.g. total revenue from Opportunities, count of active People, count of open deals). columnSpan 3–4, rowSpan 3. +- Row 3: 1–2 BAR_CHART or LINE_CHART widgets showing trends over time (group by a DATE_TIME field with MONTH granularity). columnSpan 6, rowSpan 7. +- Row 3: 1 PIE_CHART showing distribution by a SELECT field (e.g. status, type). columnSpan 6, rowSpan 7. +- Row 10: 1 STANDALONE_RICH_TEXT widget summarising the dashboard story. columnSpan 12, rowSpan 3. + +Tab 2 — "[Domain object] pipeline" (e.g. "Deals", "Applications", "Repairs"): focus on Opportunities enriched with domain data +- Before adding the RECORD_TABLE widget, run this 3-step sequence: + 1. create_view (type TABLE, name e.g. "Active Deals") → get the new viewId + 2. create_many_view_fields on the new viewId — add 4–6 key fields (name, the new stage/status SELECT, a CURRENCY/NUMERIC field, a DATE field, linked Person or Company). Use positions 0, 1, 2… and isVisible: true. + 3. create_many_view_filters + create_view_sort — e.g. filter out CLOSED/LOST records (SELECT IS_NOT "CLOSED"), sort by value DESC +- Row 0: 1 RECORD_TABLE widget. Set objectMetadataId to Opportunity, configuration.viewId to the dedicated view. columnSpan 12, rowSpan 8. +- Row 8: 1 BAR_CHART grouped by the stage SELECT field. columnSpan 6, rowSpan 7. +- Row 8: 1 PIE_CHART or AGGREGATE_CHART on the CURRENCY field. columnSpan 6, rowSpan 7. + +Tab 3 — "[Domain people role] list" (e.g. "Clients", "Candidates", "Contacts"): focus on People enriched with domain data +- Before adding the RECORD_TABLE widget, run this 3-step sequence: + 1. create_view (type TABLE, name e.g. "All Clients") → get the new viewId + 2. create_many_view_fields — add 4–5 key fields (name, email, the new SELECT/status field, a DATE field, linked Company) + 3. create_view_sort — sort by createdAt DESC or by name ASC +- Row 0: 1 RECORD_TABLE widget with the dedicated view. columnSpan 12, rowSpan 8. +- Row 8: 2–3 AGGREGATE_CHART KPIs (count, totals). columnSpan 4, rowSpan 3. +- Row 11: 1 BAR_CHART or LINE_CHART. columnSpan 12, rowSpan 7. + +After creating the dashboard, navigate to the dashboard page. `, isCustom: false, }, @@ -295,6 +391,15 @@ You help users create and manage dashboards with widgets. - STANDALONE_RICH_TEXT: configurationType "STANDALONE_RICH_TEXT" + body with markdown content - IMPORTANT: Put the actual text content in configuration.body.markdown, NOT in the widget title - Widget title should be a short label (e.g. "Notes", "Summary"), body.markdown holds the real content +- RECORD_TABLE: configurationType "RECORD_TABLE" — displays a filterable, sortable record list + - **MANDATORY 3-step pre-sequence before creating the widget**: + 1. call create_view (type TABLE, name e.g. "Repairs Dashboard Table") → get the new viewId + 2. call create_many_view_fields on the new viewId — add 4–6 of the most relevant fields (label identifier + key SELECT/DATE/CURRENCY fields). Use positions 0, 1, 2… and isVisible: true. + 3. call create_many_view_filters and/or create_view_sort on the new viewId to focus the table (e.g. filter out DONE/CANCELLED records, sort by createdAt DESC or a date field ASC) + - Never reuse a record index view — widget views and record index views must be separate + - Set objectMetadataId on the widget (top-level, required) + - Set configuration.viewId to the UUID of the dedicated view (required) + - columnSpan 12 (full width) or 6 (half width), rowSpan 6–10 Example (STANDALONE_RICH_TEXT): { @@ -302,12 +407,26 @@ Example (STANDALONE_RICH_TEXT): "body": { "markdown": "## Quarterly Summary\\n\\nKey metrics:\\n- Revenue up 15%\\n- 42 new deals closed\\n\\n**Next steps**: Focus on enterprise pipeline." } } +Example (RECORD_TABLE — always run the 3-step pre-sequence first): +Step 1 — create_view: { "name": "Active Repairs", "objectNameSingular": "repair", "type": "TABLE" } → { "id": "" } +Step 2 — create_many_view_fields: { "viewFields": [{ "viewId": "", "fieldMetadataId": "", "position": 1, "isVisible": true }, { "viewId": "", "fieldMetadataId": "", "position": 2, "isVisible": true }] } +Step 3 — create_many_view_filters: { "filters": [{ "viewId": "", "fieldMetadataId": "", "operand": "IS_NOT", "value": "DONE" }] } +Step 3b — create_view_sort: { "viewId": "", "fieldMetadataId": "", "direction": "DESC" } +Step 4 — add_dashboard_widget: { "type": "RECORD_TABLE", "objectMetadataId": "", "configuration": { "configurationType": "RECORD_TABLE", "viewId": "" }, "gridPosition": { "row": 0, "column": 0, "rowSpan": 8, "columnSpan": 12 } } + +## Tabs + +Use add_dashboard_tab to create multiple tabs in a dashboard. Each tab has its own set of widgets. +Good tab structure: one overview tab (KPIs + charts) + one or more detail tabs (RECORD_TABLE + focused charts). +After creating a tab, use its returned tabId as pageLayoutTabId when calling add_dashboard_widget. + ## Grid System - 12 columns (0-11) - KPI widgets: rowSpan 2-4, columnSpan 3-4 - Charts: rowSpan 6-8, columnSpan 6-12 -- Common layouts: 4 KPIs in a row (columnSpan 3), 2 charts side by side (columnSpan 6), full width chart (columnSpan 12) +- Record tables: rowSpan 6-10, columnSpan 6-12 (full-width preferred) +- Common layouts: 4 KPIs in a row (columnSpan 3), 2 charts side by side (columnSpan 6), full width chart or table (columnSpan 12) ## Best Practices @@ -315,7 +434,8 @@ Example (STANDALONE_RICH_TEXT): - Group related charts together - Use consistent heights within rows - Start simple, add complexity as needed -- When modifying a chart, confirm whether the user wants to change settings or change chart type`, +- When modifying a chart, confirm whether the user wants to change settings or change chart type +- Use RECORD_TABLE widgets to give users direct access to filtered record lists without leaving the dashboard`, isCustom: false, }, }), @@ -973,6 +1093,262 @@ para.paragraph_format.space_after = Pt(12) }, }), + 'view-building': (args: Omit) => + createStandardSkillFlatMetadata({ + ...args, + context: { + skillName: 'view-building', + name: 'view-building', + label: 'View Building', + description: + 'Creating and configuring views (table, board/kanban, calendar) for objects to organize and visualize records', + icon: 'IconLayoutBoard', + content: `# View Building Skill + +You help users create and configure views to organize how they see their records. + +## View Types + +- **TABLE**: Standard table/grid view. Works for any object. Default view type. +- **KANBAN**: Board view grouped by a SELECT field. Best for pipeline/status-based workflows. +- **CALENDAR**: Calendar view using a DATE or DATE_TIME field. Best for time-based records. + +## Tools + +- get_views - List existing views (filter by object name) +- create_view - Create a new view +- update_view - Update view name/icon +- delete_view - Delete a view +- create_many_view_fields - Add visible columns to a view +- update_many_view_fields - Update column configuration +- get_view_fields - List columns in a view +- list_object_metadata_items - Discover objects and their fields +- navigate_app - Navigate to a view after creation + +## Workflow + +1. **Identify the target object**: If the user didn't specify which object, ask them. Present available objects and explain what each holds: + - **Company**: Business accounts (name, domain, employees, revenue, address) + - **Person**: Contacts (name, email, phone, job title, company) + - **Opportunity**: Pipeline deals (name, stage, amount, close date, company, contact) + - **Task**: Action items (title, status, due date, assignee) + - **Note**: Free-form notes (title, body) + - Plus any custom objects in the workspace + +2. **Choose the view type**: Suggest the best type based on the object's data: + - TABLE: Good default for any object, great for browsing large datasets + - KANBAN: Ideal when objects have a SELECT field representing stages/statuses (e.g., Opportunity → stage, Task → status) + - CALENDAR: Ideal when objects have DATE/DATE_TIME fields (e.g., Opportunity → closeDate, Task → dueAt) + +3. **Create the view**: Use create_view with the right parameters. + - For KANBAN: The mainGroupByFieldName is required — ask user which SELECT field to group by, or suggest the most natural one. + - For CALENDAR: You must provide both \`calendarFieldName\` (a DATE/DATE_TIME field name) and \`calendarLayout\` ("DAY", "WEEK", or "MONTH") when calling create_view. + - For TABLE: No special configuration needed. + +4. **Configure view fields**: Use create_many_view_fields to add relevant columns. Choose fields that make sense for the view's purpose. Use decimal positions between 0 and 1 to place them after the label identifier field. + +5. **Navigate**: Use navigate_app to show the user their new view. + +## KANBAN Best Practices + +- The grouping field must be a SELECT type +- Common groupings: Opportunity by stage, Task by status +- Optionally set kanbanAggregateOperation (COUNT, SUM, AVG, MIN, MAX) and kanbanAggregateOperationFieldName for column summaries +- Example: Sum of amount per stage for Opportunity board + +## CALENDAR Best Practices + +- Requires a DATE or DATE_TIME field on the object +- Best for: Opportunity close dates, Task due dates, any event-based data + +## TABLE with Groups + +- TABLE views can also be grouped by a field using mainGroupByFieldName +- This creates collapsible sections in the table, organized by the grouping field values +- Works with SELECT fields for categorical grouping + +## Approach + +- If the user is vague (e.g., "create a board"), ask which object they want to see +- Suggest the most relevant view type based on the object's fields +- After creating a view, always configure useful view fields and navigate to it +- Explain what each view type does so users can make informed choices`, + isCustom: false, + }, + }), + + 'view-filters-and-sorts': (args: Omit) => + createStandardSkillFlatMetadata({ + ...args, + context: { + skillName: 'view-filters-and-sorts', + name: 'view-filters-and-sorts', + label: 'View Filters & Sorts', + description: + 'Adding filters and sorts to views to focus on relevant records based on user needs', + icon: 'IconFilter', + content: `# View Filters & Sorts Skill + +You help users add filters and sorts to their views so they see the most relevant records. + +## Tools + +- get_views - List existing views to find the one to modify +- get_view_query_parameters - Check existing filters and sorts on a view +- list_object_metadata_items - Discover fields and their types to build valid filters +- create_view_filter / create_many_view_filters - Add filters to a view +- create_view_sort / create_many_view_sorts - Add sorts to a view +- navigate_app - Navigate to the view to show results + +## Filter Operators by Field Type + +| Field Type | Available Operators | +|---|---| +| TEXT, EMAILS, FULL_NAME, ADDRESS, LINKS, PHONES | CONTAINS, DOES_NOT_CONTAIN, IS_EMPTY, IS_NOT_EMPTY | +| NUMBER, NUMERIC | IS, IS_NOT, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, IS_EMPTY, IS_NOT_EMPTY | +| CURRENCY | GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, IS_EMPTY, IS_NOT_EMPTY | +| DATE, DATE_TIME | IS, IS_RELATIVE, IS_IN_PAST, IS_IN_FUTURE, IS_TODAY, IS_BEFORE, IS_AFTER, IS_EMPTY, IS_NOT_EMPTY | +| SELECT | IS, IS_NOT, IS_EMPTY, IS_NOT_EMPTY | +| MULTI_SELECT, ARRAY | CONTAINS, DOES_NOT_CONTAIN, IS_EMPTY, IS_NOT_EMPTY | +| RELATION | IS, IS_NOT, IS_EMPTY, IS_NOT_EMPTY | +| BOOLEAN | IS | + +## Sort Directions + +- ASC: Ascending (A→Z, 0→9, oldest→newest) +- DESC: Descending (Z→A, 9→0, newest→oldest) + +## Filter Groups (AND/OR/NOT) + +Filters can be grouped with logical operators: +- **AND**: All filters must match (default) +- **OR**: At least one filter must match +- **NOT**: Negate the group +- Groups can be nested for complex conditions like: name CONTAINS "tech" AND (revenue > 1M OR employees > 100) + +## Workflow + +1. **Identify the view**: If the user didn't specify a view, ask which view they want to filter/sort. Use get_views to list available views and present them. + +2. **Understand the need**: If the user hasn't described what they want to see, ask them. Give guidance with examples: + - "What records do you want to focus on? For example:" + - "Show only high-value opportunities (amount > $50K)" + - "Show companies in a specific city or industry" + - "Show tasks due this week, sorted by priority" + - "Show people from a specific company" + - "Show recent records created in the last 30 days" + +3. **Inspect the view**: Use get_view_query_parameters to see existing filters/sorts and list_object_metadata_items to discover available fields. + +4. **Build filters**: Based on the user's need, determine: + - Which field(s) to filter on + - Which operator is valid for that field type (see table above) + - What value to filter by + - Whether to use AND or OR grouping for multiple filters + +5. **Build sorts**: Determine: + - Which field to sort by (most relevant to the user's goal) + - Direction: ASC or DESC + - Multiple sorts can be added (primary, secondary, etc.) + +6. **Apply and navigate**: Create the filters/sorts on the view and navigate to it. + +## Common Filter Patterns + +### By Time +- Recent records: DATE_TIME field + IS_AFTER + a date value +- Upcoming deadlines: DATE field + IS_IN_FUTURE +- Overdue tasks: DATE field + IS_IN_PAST + status IS_NOT "DONE" +- This week/month: DATE field + IS_RELATIVE + +### By Status/Stage +- Open opportunities: stage IS "IN_PROGRESS" or IS_NOT "WON"/"LOST" +- Active tasks: status IS_NOT "DONE" + +### By Relationship +- Records linked to a company: company relation IS [specific company] +- Unassigned tasks: assignee IS_EMPTY +- Orphaned records: relation field IS_EMPTY + +### By Value +- High-value deals: amount GREATER_THAN_OR_EQUAL threshold +- Large companies: employees GREATER_THAN_OR_EQUAL threshold + +## Common Sort Patterns + +- Pipeline view: Sort by amount DESC (biggest deals first) +- Task management: Sort by dueAt ASC (earliest due first) +- Recent activity: Sort by updatedAt DESC or createdAt DESC +- Alphabetical: Sort by name ASC + +## Composite Fields + +Some fields have sub-fields that can be filtered: +- CURRENCY: Use subFieldName "amountMicros" for the numeric value +- ADDRESS: Use subFieldName like "addressCity", "addressCountry" +- FULL_NAME: Use subFieldName like "firstName", "lastName" +- EMAILS: Use the primary email +- LINKS: Use the primary link URL + +## Approach + +- Always check field types before suggesting operators — using an invalid operator for a field type will fail +- When the user says "show me X", translate that into the appropriate filter logic +- Suggest sorts that complement the filters (e.g., if filtering overdue tasks, sort by dueAt ASC) +- Explain what the filters do so users understand the results +- If complex filtering is needed (AND + OR), explain the logic clearly`, + isCustom: false, + }, + }), + + 'custom-objects-cleanup': (args: Omit) => + createStandardSkillFlatMetadata({ + ...args, + context: { + skillName: 'custom-objects-cleanup', + name: 'custom-objects-cleanup', + label: 'Custom Objects Cleanup', + description: + 'Archiving custom objects from a workspace (e.g. dev seed objects like pets, rockets)', + icon: 'IconArchive', + content: `# Custom Objects Cleanup Skill + +You help users archive custom objects from their workspace, such as objects created by the dev seed (pets, rockets, survey results, etc.) or any other custom objects. + +## Tools + +- list_object_metadata_items - List all objects in the workspace to identify custom ones +- update_many_object_metadata - Archive custom objects by setting isActive to false + +## Workflow + +1. **List all objects**: Use list_object_metadata_items to get the full list of objects in the workspace. + +2. **Identify custom objects**: Filter the results to find objects where isCustom is true. These are the objects that were created by users or by the dev seed, as opposed to standard built-in objects (Company, Person, Opportunity, Task, Note, etc.). + +3. **Present findings**: Tell the user which custom objects were found. If none are found, inform the user that the workspace has no custom objects. + +4. **Confirm before archiving**: List the custom objects you found and ask the user to confirm which ones they want to archive. Present them clearly with their name, label, and description. + +5. **Archive confirmed objects**: Use update_many_object_metadata to set isActive to false on all confirmed objects in a single batch call. + +6. **Report results**: After archiving is complete, summarize what was archived. + +## Important Notes + +- Only objects with isCustom = true can be archived. Standard objects cannot be archived through this skill. +- Archiving an object hides it from the workspace but does not delete its fields, relations, or records. +- When called directly by a user, confirm before archiving. When called by another skill (e.g. workspace-demo-seeding), proceed without confirmation. + +## Approach + +- Be clear about what will be archived and that it is reversible +- If an object has relations to other objects, mention this before archiving +- Archive all confirmed objects in a single batch call using update_many_object_metadata`, + isCustom: false, + }, + }), + pptx: (args: Omit) => createStandardSkillFlatMetadata({ ...args, diff --git a/packages/twenty-server/src/modules/dashboard/tools/add-dashboard-widget.tool.ts b/packages/twenty-server/src/modules/dashboard/tools/add-dashboard-widget.tool.ts index 4e936254a5..bbed4d0106 100644 --- a/packages/twenty-server/src/modules/dashboard/tools/add-dashboard-widget.tool.ts +++ b/packages/twenty-server/src/modules/dashboard/tools/add-dashboard-widget.tool.ts @@ -22,7 +22,9 @@ const addDashboardWidgetSchema = z.object({ .string() .uuid() .optional() - .describe('Required for GRAPH widgets: object UUID to aggregate'), + .describe( + 'Required for GRAPH and RECORD_TABLE widgets: object UUID to aggregate or display', + ), configuration: widgetConfigurationSchema, }); @@ -36,7 +38,9 @@ export const createAddDashboardWidgetTool = ( Use get_dashboard first to get pageLayoutTabId and existing widget positions. Use list_object_metadata_items to get objectMetadataId and field IDs for GRAPH widgets. -See create_complete_dashboard for configuration examples.`, +For RECORD_TABLE widgets: create a dedicated view first with create_view (type TABLE), then pass its viewId in configuration. Never reuse an existing record index view. + +See create_complete_dashboard for full configuration examples.`, inputSchema: addDashboardWidgetSchema, execute: async (parameters: { pageLayoutTabId: string; diff --git a/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts b/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts index 315ce06b37..153b1bbadd 100644 --- a/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts +++ b/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts @@ -25,7 +25,7 @@ const widgetSchema = z.object({ .uuid() .optional() .describe( - 'REQUIRED for GRAPH widgets: UUID of the object to aggregate (e.g., opportunity, company)', + 'REQUIRED for GRAPH and RECORD_TABLE widgets: UUID of the object to aggregate or display', ), configuration: widgetConfigurationSchema, }); @@ -82,6 +82,14 @@ WIDGET TYPES: 6. STANDALONE_RICH_TEXT: { type: "STANDALONE_RICH_TEXT", configuration: { configurationType: "STANDALONE_RICH_TEXT", body: { ... } } } +7. RECORD_TABLE: displays a live, filterable record list directly on the dashboard. + - IMPORTANT: you MUST create a dedicated view for the widget BEFORE creating the widget. Use create_view to create a new TABLE view for the object, then pass its ID as viewId. Never reuse an existing index-page view — widget views and record index views must not overlap. + - Requires: objectMetadataId (top-level, UUID of the object to display) AND configuration.viewId (UUID of the dedicated view you just created) + - configuration.configurationType must be "RECORD_TABLE" + - Recommended size: rowSpan 8-10, columnSpan 12 (full width) + - Workflow: (1) call create_view with type TABLE for the object → get the viewId, (2) call create_many_view_fields to add visible columns to that view, (3) create the widget with that viewId + - Example: { type: "RECORD_TABLE", objectMetadataId: "", configuration: { configurationType: "RECORD_TABLE", viewId: "" } } + AGGREGATION OPERATIONS: COUNT, SUM, AVG, MIN, MAX, COUNT_EMPTY, COUNT_NOT_EMPTY`, inputSchema: createCompleteDashboardSchema, execute: async (parameters: { diff --git a/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts b/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts index bc304e3417..36ec9be8e5 100644 --- a/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts +++ b/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts @@ -191,6 +191,7 @@ export const widgetTypeSchema = z.enum([ WidgetType.GRAPH, WidgetType.IFRAME, WidgetType.STANDALONE_RICH_TEXT, + WidgetType.RECORD_TABLE, ]); // Graph configuration schema for AGGREGATE type (KPI numbers) @@ -380,6 +381,16 @@ const pieChartConfigSchema = withManualSortRefinement( }), ); +// Record table configuration +const recordTableConfigSchema = z.object({ + configurationType: z.literal(WidgetConfigurationType.RECORD_TABLE), + viewId: z + .uuid() + .describe( + 'UUID of the dedicated view created for this widget. Must be created with create_view before creating the widget. Never reuse a record index view.', + ), +}); + // Iframe configuration const iframeConfigSchema = z.object({ configurationType: z.literal(WidgetConfigurationType.IFRAME), @@ -439,6 +450,7 @@ export const widgetConfigurationSchema = z pieChartConfigSchema, iframeConfigSchema, richTextConfigSchema, + recordTableConfigSchema, ]) .optional() .describe('Widget configuration - structure depends on widget type'); @@ -451,6 +463,7 @@ export const widgetConfigurationSchemaWithoutDefaults = z pieChartConfigSchemaWithoutDefaults, iframeConfigSchema, richTextConfigSchema, + recordTableConfigSchema, ]) .optional() .describe('Widget configuration - structure depends on widget type');