2022-03-31 08:32:42 +00:00
|
|
|
//
|
2022-04-16 02:59:50 +00:00
|
|
|
// Copyright © 2022 Hardcore Engineering Inc.
|
2022-03-31 08:32:42 +00:00
|
|
|
//
|
|
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License. You may
|
|
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
//
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
//
|
|
|
|
|
2022-09-09 03:44:33 +00:00
|
|
|
import core, { Doc, DocumentUpdate, generateId, Ref, SortingOrder, TxOperations, TxResult } from '@anticrm/core'
|
2022-06-19 16:27:47 +00:00
|
|
|
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
|
2022-06-02 02:42:44 +00:00
|
|
|
import { IssueStatus, IssueStatusCategory, Team, genRanks, Issue } from '@anticrm/tracker'
|
2022-06-19 16:27:47 +00:00
|
|
|
import tags from '@anticrm/tags'
|
2022-05-16 10:38:51 +00:00
|
|
|
import { DOMAIN_TRACKER } from '.'
|
2022-04-16 02:59:50 +00:00
|
|
|
import tracker from './plugin'
|
|
|
|
|
2022-04-23 16:59:57 +00:00
|
|
|
enum DeprecatedIssueStatus {
|
|
|
|
Backlog,
|
|
|
|
Todo,
|
|
|
|
InProgress,
|
|
|
|
Done,
|
|
|
|
Canceled
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CreateTeamIssueStatusesArgs {
|
|
|
|
tx: TxOperations
|
|
|
|
teamId: Ref<Team>
|
|
|
|
categories: IssueStatusCategory[]
|
|
|
|
defaultStatusId?: Ref<IssueStatus>
|
|
|
|
defaultCategoryId?: Ref<IssueStatusCategory>
|
|
|
|
}
|
|
|
|
|
|
|
|
const categoryByDeprecatedIssueStatus = {
|
|
|
|
[DeprecatedIssueStatus.Backlog]: tracker.issueStatusCategory.Backlog,
|
|
|
|
[DeprecatedIssueStatus.Todo]: tracker.issueStatusCategory.Unstarted,
|
|
|
|
[DeprecatedIssueStatus.InProgress]: tracker.issueStatusCategory.Started,
|
|
|
|
[DeprecatedIssueStatus.Done]: tracker.issueStatusCategory.Completed,
|
|
|
|
[DeprecatedIssueStatus.Canceled]: tracker.issueStatusCategory.Canceled
|
|
|
|
} as const
|
|
|
|
|
|
|
|
async function createTeamIssueStatuses ({
|
|
|
|
tx,
|
|
|
|
teamId: attachedTo,
|
|
|
|
categories,
|
|
|
|
defaultStatusId,
|
|
|
|
defaultCategoryId = tracker.issueStatusCategory.Backlog
|
|
|
|
}: CreateTeamIssueStatusesArgs): Promise<void> {
|
|
|
|
const issueStatusRanks = [...genRanks(categories.length)]
|
|
|
|
|
|
|
|
for (const [i, statusCategory] of categories.entries()) {
|
|
|
|
const { _id: category, defaultStatusName } = statusCategory
|
|
|
|
const rank = issueStatusRanks[i]
|
|
|
|
|
|
|
|
await tx.addCollection(
|
|
|
|
tracker.class.IssueStatus,
|
|
|
|
attachedTo,
|
|
|
|
attachedTo,
|
|
|
|
tracker.class.Team,
|
|
|
|
'issueStatuses',
|
|
|
|
{ name: defaultStatusName, category, rank },
|
|
|
|
category === defaultCategoryId ? defaultStatusId : undefined
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-16 02:59:50 +00:00
|
|
|
async function createDefaultTeam (tx: TxOperations): Promise<void> {
|
|
|
|
const current = await tx.findOne(tracker.class.Team, {
|
|
|
|
_id: tracker.team.DefaultTeam
|
|
|
|
})
|
|
|
|
|
|
|
|
const currentDeleted = await tx.findOne(core.class.TxRemoveDoc, {
|
|
|
|
objectId: tracker.team.DefaultTeam
|
|
|
|
})
|
|
|
|
|
|
|
|
// Create new if not deleted by customers.
|
|
|
|
if (current === undefined && currentDeleted === undefined) {
|
2022-04-23 16:59:57 +00:00
|
|
|
const defaultStatusId: Ref<IssueStatus> = generateId()
|
2022-06-08 15:49:38 +00:00
|
|
|
const categories = await tx.findAll(
|
|
|
|
tracker.class.IssueStatusCategory,
|
|
|
|
{},
|
|
|
|
{ sort: { order: SortingOrder.Ascending } }
|
|
|
|
)
|
2022-04-23 16:59:57 +00:00
|
|
|
|
2022-04-16 02:59:50 +00:00
|
|
|
await tx.createDoc<Team>(
|
|
|
|
tracker.class.Team,
|
|
|
|
core.space.Space,
|
|
|
|
{
|
|
|
|
name: 'Default',
|
|
|
|
description: 'Default team',
|
|
|
|
private: false,
|
|
|
|
members: [],
|
|
|
|
archived: false,
|
|
|
|
identifier: 'TSK',
|
2022-04-23 16:59:57 +00:00
|
|
|
sequence: 0,
|
|
|
|
issueStatuses: 0,
|
|
|
|
defaultIssueStatus: defaultStatusId
|
2022-04-16 02:59:50 +00:00
|
|
|
},
|
|
|
|
tracker.team.DefaultTeam
|
|
|
|
)
|
2022-04-23 16:59:57 +00:00
|
|
|
await createTeamIssueStatuses({ tx, teamId: tracker.team.DefaultTeam, categories, defaultStatusId })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-08 15:49:38 +00:00
|
|
|
async function fixTeamIssueStatusesOrder (tx: TxOperations, team: Team): Promise<TxResult> {
|
|
|
|
const statuses = await tx.findAll(
|
|
|
|
tracker.class.IssueStatus,
|
|
|
|
{ attachedTo: team._id },
|
|
|
|
{ lookup: { category: tracker.class.IssueStatusCategory } }
|
|
|
|
)
|
|
|
|
statuses.sort((a, b) => (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0))
|
|
|
|
const issueStatusRanks = genRanks(statuses.length)
|
|
|
|
return statuses.map((status) => {
|
|
|
|
const rank = issueStatusRanks.next().value
|
|
|
|
if (rank === undefined || status.rank === rank) return undefined
|
|
|
|
return tx.update(status, { rank })
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fixTeamsIssueStatusesOrder (tx: TxOperations): Promise<void> {
|
|
|
|
const teams = await tx.findAll(tracker.class.Team, {})
|
|
|
|
await Promise.all(teams.map((team) => fixTeamIssueStatusesOrder(tx, team)))
|
|
|
|
}
|
|
|
|
|
2022-04-23 16:59:57 +00:00
|
|
|
async function upgradeTeamIssueStatuses (tx: TxOperations): Promise<void> {
|
|
|
|
const teams = await tx.findAll(tracker.class.Team, { issueStatuses: undefined })
|
|
|
|
|
|
|
|
if (teams.length > 0) {
|
2022-06-08 15:49:38 +00:00
|
|
|
const categories = await tx.findAll(
|
|
|
|
tracker.class.IssueStatusCategory,
|
|
|
|
{},
|
|
|
|
{ sort: { order: SortingOrder.Ascending } }
|
|
|
|
)
|
2022-04-23 16:59:57 +00:00
|
|
|
|
|
|
|
for (const team of teams) {
|
|
|
|
const defaultStatusId: Ref<IssueStatus> = generateId()
|
|
|
|
|
|
|
|
await tx.update(team, { issueStatuses: 0, defaultIssueStatus: defaultStatusId })
|
|
|
|
await createTeamIssueStatuses({ tx, teamId: team._id, categories, defaultStatusId })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function upgradeIssueStatuses (tx: TxOperations): Promise<void> {
|
|
|
|
const deprecatedStatuses = [
|
|
|
|
DeprecatedIssueStatus.Backlog,
|
|
|
|
DeprecatedIssueStatus.Canceled,
|
|
|
|
DeprecatedIssueStatus.Done,
|
|
|
|
DeprecatedIssueStatus.InProgress,
|
|
|
|
DeprecatedIssueStatus.Todo
|
|
|
|
]
|
|
|
|
const issues = await tx.findAll(tracker.class.Issue, { status: { $in: deprecatedStatuses as any } })
|
|
|
|
|
|
|
|
if (issues.length > 0) {
|
|
|
|
const statusByDeprecatedStatus = new Map<DeprecatedIssueStatus, Ref<IssueStatus>>()
|
|
|
|
|
|
|
|
for (const issue of issues) {
|
|
|
|
const deprecatedStatus = issue.status as unknown as DeprecatedIssueStatus
|
|
|
|
|
|
|
|
if (!statusByDeprecatedStatus.has(deprecatedStatus)) {
|
|
|
|
const category = categoryByDeprecatedIssueStatus[deprecatedStatus]
|
|
|
|
const issueStatus = await tx.findOne(tracker.class.IssueStatus, { category })
|
|
|
|
|
|
|
|
if (issueStatus === undefined) {
|
|
|
|
throw new Error(`Could not find a new status for "${DeprecatedIssueStatus[deprecatedStatus]}"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
statusByDeprecatedStatus.set(deprecatedStatus, issueStatus._id)
|
|
|
|
}
|
|
|
|
|
|
|
|
await tx.update(issue, { status: statusByDeprecatedStatus.get(deprecatedStatus) })
|
|
|
|
}
|
2022-04-16 02:59:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-02 02:42:44 +00:00
|
|
|
async function migrateParentIssues (client: MigrationClient): Promise<void> {
|
|
|
|
let { updated } = await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, attachedToClass: { $exists: false } },
|
|
|
|
{
|
|
|
|
subIssues: 0,
|
|
|
|
collection: 'subIssues',
|
|
|
|
attachedToClass: tracker.class.Issue
|
|
|
|
}
|
|
|
|
)
|
|
|
|
updated += (
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, parentIssue: { $exists: true } },
|
|
|
|
{ $rename: { parentIssue: 'attachedTo' } }
|
|
|
|
)
|
|
|
|
).updated
|
|
|
|
updated += (
|
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, attachedTo: { $in: [null, undefined] } },
|
|
|
|
{ attachedTo: tracker.ids.NoParent }
|
|
|
|
)
|
|
|
|
).updated
|
|
|
|
|
|
|
|
if (updated === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const childrenCountById = new Map<Ref<Doc>, number>()
|
|
|
|
const parentIssueIds = (
|
|
|
|
await client.find<Issue>(DOMAIN_TRACKER, {
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
attachedTo: { $nin: [tracker.ids.NoParent] }
|
|
|
|
})
|
|
|
|
).map((issue) => issue.attachedTo)
|
|
|
|
|
|
|
|
for (const issueId of parentIssueIds) {
|
|
|
|
const count = childrenCountById.get(issueId) ?? 0
|
|
|
|
childrenCountById.set(issueId, count + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [_id, childrenCount] of childrenCountById) {
|
|
|
|
await client.update(DOMAIN_TRACKER, { _id }, { subIssues: childrenCount })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-27 06:04:26 +00:00
|
|
|
async function updateIssueParentInfo (client: MigrationClient, parentIssue: Issue | null): Promise<void> {
|
|
|
|
const parents =
|
|
|
|
parentIssue === null ? [] : [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
|
|
|
|
const migrationResult = await client.update<Issue>(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
|
|
|
|
parents: { $exists: false }
|
|
|
|
},
|
|
|
|
{ parents }
|
|
|
|
)
|
|
|
|
|
|
|
|
if (migrationResult.matched > 0) {
|
|
|
|
const subIssues = await client.find<Issue>(DOMAIN_TRACKER, {
|
|
|
|
_class: tracker.class.Issue,
|
|
|
|
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
|
|
|
|
subIssues: { $gt: 0 }
|
|
|
|
})
|
|
|
|
|
|
|
|
for (const issue of subIssues) {
|
|
|
|
await updateIssueParentInfo(client, issue)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function migrateIssueParentInfo (client: MigrationClient): Promise<void> {
|
|
|
|
await updateIssueParentInfo(client, null)
|
|
|
|
}
|
|
|
|
|
2022-05-16 10:38:51 +00:00
|
|
|
async function migrateIssueProjects (client: MigrationClient): Promise<void> {
|
|
|
|
const issues = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue, project: { $exists: false } })
|
|
|
|
|
|
|
|
if (issues.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const issue of issues) {
|
|
|
|
await client.update(DOMAIN_TRACKER, { _id: issue._id }, { project: null })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function upgradeProjectIcons (tx: TxOperations): Promise<void> {
|
|
|
|
const projects = await tx.findAll(tracker.class.Project, {})
|
|
|
|
|
|
|
|
if (projects.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const project of projects) {
|
|
|
|
const icon = project.icon as unknown
|
|
|
|
|
|
|
|
if (icon !== undefined) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
await tx.update(project, { icon: tracker.icon.Projects })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-16 02:59:50 +00:00
|
|
|
async function createDefaults (tx: TxOperations): Promise<void> {
|
|
|
|
await createDefaultTeam(tx)
|
2022-06-19 16:27:47 +00:00
|
|
|
await createOrUpdate(
|
|
|
|
tx,
|
|
|
|
tags.class.TagCategory,
|
|
|
|
tags.space.Tags,
|
|
|
|
{
|
|
|
|
icon: tags.icon.Tags,
|
|
|
|
label: 'Other',
|
|
|
|
targetClass: tracker.class.Issue,
|
|
|
|
tags: [],
|
|
|
|
default: true
|
|
|
|
},
|
|
|
|
tracker.category.Other
|
|
|
|
)
|
2022-04-16 02:59:50 +00:00
|
|
|
}
|
2022-03-31 08:32:42 +00:00
|
|
|
|
2022-04-23 16:59:57 +00:00
|
|
|
async function upgradeTeams (tx: TxOperations): Promise<void> {
|
|
|
|
await upgradeTeamIssueStatuses(tx)
|
2022-06-08 15:49:38 +00:00
|
|
|
await fixTeamsIssueStatusesOrder(tx)
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function upgradeIssues (tx: TxOperations): Promise<void> {
|
|
|
|
await upgradeIssueStatuses(tx)
|
2022-09-09 03:44:33 +00:00
|
|
|
|
|
|
|
const issues = await tx.findAll(tracker.class.Issue, {
|
|
|
|
$or: [{ blockedBy: { $exists: true } }, { relatedIssue: { $exists: true } }]
|
|
|
|
})
|
|
|
|
|
|
|
|
for (const i of issues) {
|
|
|
|
const rel = (i as any).relatedIssue as Ref<Issue>[]
|
|
|
|
const upd: DocumentUpdate<Issue> = {}
|
|
|
|
if (rel != null) {
|
|
|
|
;(upd as any).relatedIssue = null
|
|
|
|
upd.relations = rel.map((it) => ({ _id: it, _class: tracker.class.Issue }))
|
|
|
|
}
|
|
|
|
if (i.blockedBy !== undefined) {
|
|
|
|
if ((i.blockedBy as any[]).find((it) => typeof it === 'string') !== undefined) {
|
|
|
|
upd.blockedBy = (i.blockedBy as unknown as Ref<Issue>[]).map((it) => ({ _id: it, _class: tracker.class.Issue }))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (Object.keys(upd).length > 0) {
|
|
|
|
await tx.update(i, upd)
|
|
|
|
}
|
|
|
|
}
|
2022-04-23 16:59:57 +00:00
|
|
|
}
|
|
|
|
|
2022-05-16 10:38:51 +00:00
|
|
|
async function upgradeProjects (tx: TxOperations): Promise<void> {
|
|
|
|
await upgradeProjectIcons(tx)
|
|
|
|
}
|
|
|
|
|
2022-03-31 08:32:42 +00:00
|
|
|
export const trackerOperation: MigrateOperation = {
|
2022-05-16 10:38:51 +00:00
|
|
|
async migrate (client: MigrationClient): Promise<void> {
|
2022-08-16 10:19:33 +00:00
|
|
|
await client.update(
|
|
|
|
DOMAIN_TRACKER,
|
|
|
|
{ _class: tracker.class.Issue, reports: { $exists: false } },
|
|
|
|
{
|
|
|
|
reports: 0,
|
|
|
|
estimation: 0,
|
|
|
|
reportedTime: 0
|
|
|
|
}
|
|
|
|
)
|
2022-06-02 02:42:44 +00:00
|
|
|
await Promise.all([migrateIssueProjects(client), migrateParentIssues(client)])
|
2022-06-27 06:04:26 +00:00
|
|
|
await migrateIssueParentInfo(client)
|
2022-05-16 10:38:51 +00:00
|
|
|
},
|
2022-03-31 08:32:42 +00:00
|
|
|
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
2022-04-16 02:59:50 +00:00
|
|
|
const tx = new TxOperations(client, core.account.System)
|
|
|
|
await createDefaults(tx)
|
2022-04-23 16:59:57 +00:00
|
|
|
await upgradeTeams(tx)
|
|
|
|
await upgradeIssues(tx)
|
2022-05-16 10:38:51 +00:00
|
|
|
await upgradeProjects(tx)
|
2022-03-31 08:32:42 +00:00
|
|
|
}
|
|
|
|
}
|