From 5b961cbb7f67a0de0b7be5bf044fcc2fc59319c7 Mon Sep 17 00:00:00 2001 From: eliasylonen Date: Mon, 17 Feb 2025 11:13:55 +0100 Subject: [PATCH] Tasks assigned to me view (#9567) (#9568) Issue: https://github.com/twentyhq/core-team-issues/issues/154 View is now added. TODO: Fix filtering a relation field by 'Me' in Tasks. --------- Co-authored-by: ad-elias --- ...ndexRecordGroupHideComponentFamilyState.ts | 2 +- .../commands/database-command.module.ts | 2 + ...3-add-tasks-assigned-to-me-view.command.ts | 243 ++++++++++++++++++ .../0-43/0-43-upgrade-version.command.ts | 37 +++ .../0-43/0-43-upgrade-version.module.ts | 26 ++ .../seed-view-with-demo-data.ts | 2 + .../views/tasks-assigned-to-me.ts | 148 +++++++++++ .../views/tasks-by-status.view.ts | 4 +- 8 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me.ts diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts index da9bed69c..1f75f52f4 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts @@ -10,7 +10,7 @@ export const recordIndexRecordGroupHideComponentFamilyState = case ViewType.Kanban: return false; case ViewType.Table: - return true; + return false; default: return false; } diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index 4a70026e8..23c751bac 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -10,6 +10,7 @@ import { ConfirmationQuestion } from 'src/database/commands/questions/confirmati import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module'; import { UpgradeTo0_41CommandModule } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module'; import { UpgradeTo0_42CommandModule } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module'; +import { UpgradeTo0_43CommandModule } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -53,6 +54,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp UpgradeTo0_40CommandModule, UpgradeTo0_41CommandModule, UpgradeTo0_42CommandModule, + UpgradeTo0_43CommandModule, FeatureFlagModule, ], providers: [ diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command.ts new file mode 100644 index 000000000..790da0990 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command.ts @@ -0,0 +1,243 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { isCommandLogger } from 'src/database/commands/logger'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; +import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; +import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; +import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; + +@Command({ + name: 'upgrade-0.43:add-tasks-assigned-to-me-view', + description: 'Add tasks assigned to me view', +}) +export class AddTasksAssignedToMeViewCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log('Running command to create many to one relations'); + + if (isCommandLogger(this.logger)) { + this.logger.setVerbose(options.verbose ?? false); + } + + try { + for (const [index, workspaceId] of workspaceIds.entries()) { + await this.processWorkspace(workspaceId, index, workspaceIds.length); + } + + this.logger.log(chalk.green('Command completed!')); + } catch (error) { + this.logger.log(chalk.red('Error in workspace')); + } + } + + private async processWorkspace( + workspaceId: string, + index: number, + total: number, + ): Promise { + try { + this.logger.log( + `Running command for workspace ${workspaceId} ${index + 1}/${total}`, + ); + + const viewId = await this.createTasksAssignedToMeView(workspaceId); + + await this.createTasksAssignedToMeViewGroups(workspaceId, viewId); + + await this.workspaceMetadataVersionService.incrementMetadataVersion( + workspaceId, + ); + + this.logger.log( + chalk.green(`Command completed for workspace ${workspaceId}.`), + ); + } catch { + this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`)); + } + } + + private async createTasksAssignedToMeView( + workspaceId: string, + ): Promise { + const objectMetadata = await this.objectMetadataRepository.find({ + where: { workspaceId }, + relations: ['fields'], + }); + + const objectMetadataMap = objectMetadata.reduce((acc, object) => { + acc[object.standardId ?? ''] = { + id: object.id, + fields: object.fields.reduce((acc, field) => { + acc[field.standardId ?? ''] = field.id; + + return acc; + }, {}), + }; + + return acc; + }, {}); + + const taskObjectMetadata = objectMetadata.find( + (object) => object.standardId === STANDARD_OBJECT_IDS.task, + ); + + if (!taskObjectMetadata) { + throw new Error(`Task object not found for workspace ${workspaceId}`); + } + + const viewRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'view', + false, + ); + + const existingView = await viewRepository.findOne({ + where: { + name: 'Assigned to Me', + objectMetadataId: taskObjectMetadata.id, + }, + }); + + if (existingView) { + throw new Error( + `"Assigned to Me" view already exists for workspace ${workspaceId}`, + ); + } + + const viewDefinition = tasksAssignedToMeView(objectMetadataMap); + const viewId = v4(); + + const insertedView = await viewRepository.save({ + id: viewId, + name: viewDefinition.name, + objectMetadataId: viewDefinition.objectMetadataId, + type: viewDefinition.type, + position: viewDefinition.position, + icon: viewDefinition.icon, + kanbanFieldMetadataId: viewDefinition.kanbanFieldMetadataId, + }); + + if (viewDefinition.fields && viewDefinition.fields.length > 0) { + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewField', + false, + ); + + const viewFields = viewDefinition.fields.map((field) => ({ + fieldMetadataId: field.fieldMetadataId, + position: field.position, + isVisible: field.isVisible, + size: field.size, + viewId: insertedView.id, + })); + + await viewFieldRepository.save(viewFields); + } + + if (viewDefinition.filters && viewDefinition.filters.length > 0) { + const viewFilterRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewFilter', + false, + ); + + const viewFilters = viewDefinition.filters.map((filter) => ({ + fieldMetadataId: filter.fieldMetadataId, + displayValue: filter.displayValue, + operand: filter.operand, + value: filter.value, + viewId: insertedView.id, + })); + + await viewFilterRepository.save(viewFilters); + } + + return insertedView.id; + } + + private async createTasksAssignedToMeViewGroups( + workspaceId: string, + viewId: string, + ) { + const taskStatusFieldMetadata = await this.fieldMetadataRepository.findOne({ + where: { + workspaceId, + standardId: TASK_STANDARD_FIELD_IDS.status, + }, + }); + + if (!taskStatusFieldMetadata) { + throw new Error( + `Task status field metadata not found for workspace ${workspaceId}`, + ); + } + + const optionValueViewGroups = taskStatusFieldMetadata.options.map( + (taskStatusOption: FieldMetadataDefaultOption, index) => + ({ + fieldMetadataId: taskStatusFieldMetadata.id, + viewId, + fieldValue: taskStatusOption.value, + position: index, + }) satisfies Partial, + ); + + const noValueViewGroup: Partial = { + fieldMetadataId: taskStatusFieldMetadata.id, + viewId, + fieldValue: '', + position: optionValueViewGroups.length, + }; + + const viewGroups = [...optionValueViewGroups, noValueViewGroup]; + + const viewGroupRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewGroup', + false, + ); + + await viewGroupRepository.insert(viewGroups); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts new file mode 100644 index 000000000..ac2d59eb7 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command.ts @@ -0,0 +1,37 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; +import { BaseCommandOptions } from 'src/database/commands/base.command'; +import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Command({ + name: 'upgrade-0.43', + description: 'Upgrade to 0.43', +}) +export class UpgradeTo0_43Command extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + private readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + passedParam: string[], + options: BaseCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log('Running command to upgrade to 0.43'); + + await this.addTasksAssignedToMeViewCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts new file mode 100644 index 000000000..9ba25d5d5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command'; +import { UpgradeTo0_43Command } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), + WorkspaceMigrationRunnerModule, + WorkspaceMigrationModule, + WorkspaceMetadataVersionModule, + ], + providers: [UpgradeTo0_43Command, AddTasksAssignedToMeViewCommand], +}) +export class UpgradeTo0_43CommandModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data.ts index 5030b01cd..49dc57dfa 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/seed-view-with-demo-data.ts @@ -9,6 +9,7 @@ import { opportunitiesAllView } from 'src/engine/workspace-manager/standard-obje import { opportunitiesByStageView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view'; import { peopleAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view'; import { tasksAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view'; +import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me'; import { tasksByStatusView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view'; import { workflowRunsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view'; import { workflowVersionsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view'; @@ -26,6 +27,7 @@ export const seedViewWithDemoData = async ( opportunitiesByStageView(objectMetadataStandardIdToIdMap), notesAllView(objectMetadataStandardIdToIdMap), tasksAllView(objectMetadataStandardIdToIdMap), + tasksAssignedToMeView(objectMetadataStandardIdToIdMap), tasksByStatusView(objectMetadataStandardIdToIdMap), workflowsAllView(objectMetadataStandardIdToIdMap), workflowVersionsAllView(objectMetadataStandardIdToIdMap), diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me.ts new file mode 100644 index 000000000..caf6ffb2c --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me.ts @@ -0,0 +1,148 @@ +import { ObjectMetadataStandardIdToIdMap } from 'src/engine/metadata-modules/object-metadata/interfaces/object-metadata-standard-id-to-id-map'; + +import { + BASE_OBJECT_STANDARD_FIELD_IDS, + TASK_STANDARD_FIELD_IDS, +} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; + +export const tasksAssignedToMeView = ( + objectMetadataStandardIdToIdMap: ObjectMetadataStandardIdToIdMap, +) => { + return { + name: 'Assigned to Me', + objectMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].id, + type: 'table', + key: null, + position: 2, + icon: 'IconUserCircle', + kanbanFieldMetadataId: '', + filters: [ + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.assignee + ], + displayValue: 'Me', + operand: 'is', + value: JSON.stringify({ + isCurrentWorkspaceMemberSelected: true, + selectedRecordIds: [], + }), + }, + ], + fields: [ + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.title + ], + position: 0, + isVisible: true, + size: 210, + }, + /*{ + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + position: 2, + isVisible: true, + size: 150, + },*/ + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.taskTargets + ], + position: 3, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.createdBy + ], + position: 4, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.dueAt + ], + position: 5, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.assignee + ], + position: 6, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.body + ], + position: 7, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + BASE_OBJECT_STANDARD_FIELD_IDS.createdAt + ], + position: 8, + isVisible: true, + size: 150, + }, + ], + groups: [ + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + isVisible: true, + fieldValue: 'TODO', + position: 0, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + isVisible: true, + fieldValue: 'IN_PROGRESS', + position: 1, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + isVisible: true, + fieldValue: 'DONE', + position: 2, + }, + { + fieldMetadataId: + objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[ + TASK_STANDARD_FIELD_IDS.status + ], + isVisible: true, + fieldValue: '', + position: 3, + }, + ], + }; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts index db0461f9a..2bfca9245 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts @@ -10,12 +10,12 @@ export const tasksByStatusView = ( objectMetadataStandardIdToIdMap: ObjectMetadataStandardIdToIdMap, ) => { return { - name: 'By status', + name: 'By Status', objectMetadataId: objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].id, type: 'kanban', key: null, - position: 0, + position: 1, icon: 'IconLayoutKanban', kanbanFieldMetadataId: objectMetadataStandardIdToIdMap[STANDARD_OBJECT_IDS.task].fields[