From 16204edaac499924b0738a42e1ca9629d618494b Mon Sep 17 00:00:00 2001
From: Alexey Zinoviev <alexey.zinoviev@xored.com>
Date: Fri, 29 Mar 2024 17:23:32 +0400
Subject: [PATCH] uberf-6224: restore missing task types (#5094)

---
 models/task/src/migration.ts    |   4 +-
 models/tracker/src/migration.ts | 135 +++++++++++++++++++++++++++++++-
 2 files changed, 136 insertions(+), 3 deletions(-)

diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts
index 1c674e1605..b2549a0c20 100644
--- a/models/task/src/migration.ts
+++ b/models/task/src/migration.ts
@@ -203,7 +203,7 @@ async function migrateProjectTypes (client: MigrationClient): Promise<void> {
       modifiedOn: pt.modifiedOn,
       createdBy: pt.createdBy,
       createdOn: pt.createdOn,
-      modifiedBy: pt.modifiedBy
+      modifiedBy: pt.modifiedBy !== core.account.System ? pt.modifiedBy : core.account.ConfigUser
     }
     await client.create(DOMAIN_TX, tx)
 
@@ -241,7 +241,7 @@ async function migrateTaskTypes (client: MigrationClient): Promise<void> {
       modifiedOn: tt.modifiedOn,
       createdBy: tt.createdBy,
       createdOn: tt.createdOn,
-      modifiedBy: tt.modifiedBy
+      modifiedBy: tt.modifiedBy !== core.account.System ? tt.modifiedBy : core.account.ConfigUser
     }
     await client.create(DOMAIN_TX, tx)
 
diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts
index 016f0132a8..ff0966f503 100644
--- a/models/tracker/src/migration.ts
+++ b/models/tracker/src/migration.ts
@@ -24,7 +24,9 @@ import core, {
   type Ref,
   type Status,
   type Tx,
-  type TxCreateDoc
+  type TxCreateDoc,
+  type StatusCategory,
+  type TxMixin
 } from '@hcengineering/core'
 import {
   createOrUpdate,
@@ -365,6 +367,133 @@ async function migrateIdentifiers (client: MigrationClient): Promise<void> {
   }
 }
 
+async function restoreTaskTypes (client: MigrationClient): Promise<void> {
+  // Query all tracker project types creations (in Model)
+  // We only update new project types in model here and not old ones in spaces
+  const projectTypes = (await client.find(DOMAIN_TX, {
+    _class: core.class.TxCreateDoc,
+    objectClass: task.class.ProjectType,
+    objectSpace: core.space.Model,
+    'attributes.descriptor': tracker.descriptors.ProjectType
+  })) as TxCreateDoc<ProjectType>[]
+
+  if (projectTypes.length === 0) {
+    return
+  }
+
+  const descr = client.model.getObject(tracker.descriptors.ProjectType)
+  const knownCategories = classicIssueTaskStatuses.map((c) => c.category)
+
+  function compareCategories (a: Ref<StatusCategory>, b: Ref<StatusCategory>): number {
+    const indexOfA = knownCategories.indexOf(a)
+    const indexOfB = knownCategories.indexOf(b)
+
+    return indexOfA - indexOfB
+  }
+
+  for (const projType of projectTypes) {
+    for (const taskTypeId of projType.attributes.tasks) {
+      // Check if task type create TX exists
+      const createTx = (
+        (await client.find(DOMAIN_TX, {
+          _class: core.class.TxCreateDoc,
+          objectClass: task.class.TaskType,
+          objectSpace: core.space.Model,
+          objectId: taskTypeId
+        })) as TxCreateDoc<TaskType>[]
+      )[0]
+
+      if (createTx !== undefined) {
+        continue
+      }
+
+      // Restore create task type tx
+
+      // Get target class mixin
+
+      const typeMixin = (
+        await client.find(DOMAIN_TX, {
+          mixin: task.mixin.TaskTypeClass,
+          'attributes.projectType': projType.objectId,
+          'attributes.taskType': taskTypeId
+        })
+      )[0] as TxMixin<any, any>
+
+      if (typeMixin === undefined) {
+        throw new Error('No type mixin found for the task type being restored')
+      }
+
+      // Get statuses and categories
+      const statusesIds = projType.attributes.statuses.filter((s) => s.taskType === taskTypeId).map((s) => s._id)
+      if (statusesIds.length === 0) {
+        throw new Error('No statuses defined for the task type being restored')
+      }
+      const statuses = await client.find<Status>(DOMAIN_STATUS, {
+        _id: { $in: statusesIds }
+      })
+      const categoriesIds = new Set<Ref<StatusCategory>>()
+
+      statuses.forEach((st) => {
+        if (st.category !== undefined) {
+          categoriesIds.add(st.category)
+        }
+      })
+
+      if (categoriesIds.size === 0) {
+        throw new Error('No categories found for the task type being restored')
+      }
+
+      const statusCategories = Array.from(categoriesIds)
+
+      statusCategories.sort(compareCategories)
+
+      const createTxNew: TxCreateDoc<TaskType> = {
+        _id: generateId(),
+        _class: core.class.TxCreateDoc,
+        space: core.space.Tx,
+        objectId: taskTypeId,
+        objectClass: task.class.TaskType,
+        objectSpace: core.space.Model,
+        modifiedBy: core.account.ConfigUser, // So it's not removed during the next migration
+        modifiedOn: projType.modifiedOn,
+        createdOn: projType.createdOn,
+        attributes: {
+          name: 'Issue',
+          descriptor: tracker.descriptors.Issue,
+          ofClass: tracker.class.Issue,
+          targetClass: typeMixin.objectId,
+          statusClass: tracker.class.IssueStatus,
+          allowedAsChildOf: [taskTypeId],
+          statuses: statusesIds,
+          statusCategories,
+          parent: projType.objectId,
+          kind: 'both',
+          icon: descr.icon
+        }
+      }
+
+      await client.create(DOMAIN_TX, createTxNew)
+
+      // If there were updates to the task type - move them to the model
+      // Check if task type create TX exists
+      const updateTxes = await client.find(DOMAIN_TX, {
+        _class: { $in: [core.class.TxUpdateDoc, core.class.TxRemoveDoc] },
+        objectClass: task.class.TaskType,
+        objectSpace: projType.objectId,
+        objectId: taskTypeId
+      })
+
+      for (const updTx of updateTxes) {
+        await client.create<Tx>(DOMAIN_TX, {
+          ...updTx,
+          _id: generateId(),
+          objectSpace: core.space.Model
+        })
+      }
+    }
+  }
+}
+
 export const trackerOperation: MigrateOperation = {
   async migrate (client: MigrationClient): Promise<void> {
     await tryMigrate(client, 'tracker', [
@@ -504,6 +633,10 @@ export const trackerOperation: MigrateOperation = {
       {
         state: 'passIdentifierToParentInfo',
         func: passIdentifierToParentInfo
+      },
+      {
+        state: 'restoreTaskTypes',
+        func: restoreTaskTypes
       }
     ])
   },