UBERF-4248: Task type (#4042)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2023-12-14 23:26:02 +07:00
committed by GitHub
parent 58b841518a
commit 19d6764250
196 changed files with 4690 additions and 2524 deletions
+403 -24
View File
@@ -16,18 +16,25 @@
import type { Employee, Person } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import {
type Class,
ClassifierKind,
DOMAIN_MODEL,
DOMAIN_STATUS,
DOMAIN_TX,
IndexKind,
generateId,
type Class,
type Data,
type Doc,
type Domain,
IndexKind,
type Ref,
type Status,
type StatusCategory,
type Timestamp
type Timestamp,
type TxCreateDoc,
type TxMixin
} from '@hcengineering/core'
import {
type Builder,
ArrOf,
Collection,
Hidden,
Index,
@@ -37,34 +44,50 @@ import {
TypeBoolean,
TypeDate,
TypeMarkup,
TypeRecord,
TypeRef,
TypeString,
UX
UX,
type Builder,
type MigrationClient
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import { type IntlString } from '@hcengineering/platform'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import chunter from '@hcengineering/model-chunter'
import core, { DOMAIN_SPACE, TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
import view, {
classPresenter,
createAction,
template,
actionTemplates as viewTemplates
} from '@hcengineering/model-view'
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import tags from '@hcengineering/tags'
import {
calculateStatuses,
findStatusAttr,
type KanbanCard,
type Project,
type ProjectStatus,
type ProjectType,
type ProjectTypeCategory,
type ProjectTypeClass,
type ProjectTypeDescriptor,
type Sequence,
type Task,
type TaskType,
type TaskTypeClass,
type TaskTypeDescriptor,
type TaskTypeKind,
type TodoItem
} from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import type { AnyComponent } from '@hcengineering/ui'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import { type ViewAction } from '@hcengineering/view'
import chunter from '@hcengineering/model-chunter'
import task from './plugin'
export { taskId } from '@hcengineering/task'
export { createProjectType, createSequence, taskOperation } from './migration'
export { createProjectType, taskId } from '@hcengineering/task'
export { createSequence, taskOperation } from './migration'
export { default } from './plugin'
export const DOMAIN_TASK = 'task' as Domain
@@ -80,6 +103,10 @@ export class TTask extends TAttachedDoc implements Task {
@Index(IndexKind.Indexed)
status!: Ref<Status>
@Prop(TypeRef(task.class.TaskType), task.string.TaskType)
@Index(IndexKind.Indexed)
kind!: Ref<TaskType>
@Prop(TypeString(), task.string.TaskNumber)
@Index(IndexKind.FullText)
@Hidden()
@@ -132,27 +159,91 @@ export class TKanbanCard extends TClass implements KanbanCard {
card!: AnyComponent
}
@Model(task.class.TaskTypeDescriptor, core.class.Doc, DOMAIN_MODEL)
export class TTaskTypeDescriptor extends TDoc implements TaskTypeDescriptor {
name!: IntlString
description!: IntlString
icon!: Asset
baseClass!: Ref<Class<Task>>
// If specified, will allow to be created by users, system type overwize
allowCreate!: boolean
}
@Mixin(task.mixin.TaskTypeClass, core.class.Class)
export class TTaskTypeClass extends TClass implements TaskTypeClass {
taskType!: Ref<TaskType>
projectType!: Ref<ProjectType>
}
@Mixin(task.mixin.ProjectTypeClass, core.class.Class)
export class TProjectTypeClass extends TClass implements ProjectTypeClass {
projectType!: Ref<ProjectType>
}
@Model(task.class.Project, core.class.Space)
export class TProject extends TSpace implements Project {
type!: Ref<ProjectType>
@Prop(TypeRef(task.class.ProjectType), task.string.ProjectType)
type!: Ref<ProjectType>
}
@Model(task.class.ProjectType, core.class.Space)
export class TProjectType extends TSpace implements ProjectType {
statuses!: ProjectStatus[]
shortDescription?: string
category!: Ref<ProjectTypeCategory>
@Prop(TypeRef(task.class.ProjectTypeDescriptor), getEmbeddedLabel('Descriptor'))
descriptor!: Ref<ProjectTypeDescriptor>
@Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Tasks'))
tasks!: Ref<TaskType>[]
@Prop(ArrOf(TypeRecord()), getEmbeddedLabel('Project statuses'))
statuses!: ProjectStatus[]
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Target Class'))
targetClass!: Ref<Class<Project>>
}
@Model(task.class.ProjectTypeCategory, core.class.Doc, DOMAIN_MODEL)
export class TProjectTypeCategory extends TDoc implements ProjectTypeCategory {
@Model(task.class.TaskType, core.class.Doc, DOMAIN_TASK)
export class TTaskType extends TDoc implements TaskType {
@Prop(TypeString(), getEmbeddedLabel('Name'))
name!: string
@Prop(TypeRef(task.class.TaskTypeDescriptor), getEmbeddedLabel('Descriptor'))
descriptor!: Ref<TaskTypeDescriptor>
@Prop(TypeRef(task.class.ProjectType), getEmbeddedLabel('Task class'))
parent!: Ref<ProjectType> // Base class for task
@Prop(TypeString(), getEmbeddedLabel('Kind'))
kind!: TaskTypeKind
@Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Parent'))
allowedAsChildOf!: Ref<TaskType>[] // In case of specified, task type is for sub-tasks
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task class'))
ofClass!: Ref<Class<Task>> // Base class for task
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task target class'))
targetClass!: Ref<Class<Task>> // Class or Mixin mixin to hold all user defined attributes.
@Prop(ArrOf(TypeRef(core.class.Status)), getEmbeddedLabel('Task statuses'))
statuses!: Ref<Status>[]
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task status class'))
statusClass!: Ref<Class<Status>>
@Prop(TypeRef(core.class.StatusCategory), getEmbeddedLabel('Task status categories'))
statusCategories!: Ref<StatusCategory>[]
}
@Model(task.class.ProjectTypeDescriptor, core.class.Doc, DOMAIN_MODEL)
export class TProjectTypeDescriptor extends TDoc implements ProjectTypeDescriptor {
name!: IntlString
description!: IntlString
icon!: AnyComponent
editor?: AnyComponent
attachedToClass!: Ref<Class<Project>>
statusClass!: Ref<Class<Status>>
statusCategories!: Ref<StatusCategory>[]
baseClass!: Ref<Class<Task>>
}
@Model(task.class.Sequence, core.class.Doc, DOMAIN_KANBAN)
@@ -218,7 +309,19 @@ export const actionTemplates = template({
})
export function createModel (builder: Builder): void {
builder.createModel(TKanbanCard, TSequence, TTask, TTodoItem, TProject, TProjectType, TProjectTypeCategory)
builder.createModel(
TKanbanCard,
TSequence,
TTask,
TTodoItem,
TProject,
TProjectType,
TTaskType,
TProjectTypeDescriptor,
TTaskTypeDescriptor,
TTaskTypeClass,
TProjectTypeClass
)
builder.createDoc(
view.class.ViewletDescriptor,
@@ -243,6 +346,10 @@ export function createModel (builder: Builder): void {
editor: task.component.TaskHeader
})
builder.mixin(task.class.ProjectType, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
@@ -312,6 +419,16 @@ export function createModel (builder: Builder): void {
presenter: task.component.TodoItemPresenter
})
builder.mixin(task.class.TaskType, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.TaskTypePresenter
})
builder.mixin(task.class.ProjectType, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.ProjectTypePresenter
})
classPresenter(builder, task.class.TaskType, task.component.TaskTypePresenter, task.component.TaskTypePresenter)
createAction(builder, {
label: task.string.MarkAsDone,
icon: task.icon.TodoCheck,
@@ -370,7 +487,21 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
ofAttribute: task.attribute.State,
label: core.string.Status,
label: task.string.StateBacklog,
icon: task.icon.TaskState,
color: PaletteColorIndexes.Coin,
defaultStatusName: 'Backlog',
order: 0
},
task.statusCategory.UnStarted
)
builder.createDoc(
core.class.StatusCategory,
core.space.Model,
{
ofAttribute: task.attribute.State,
label: task.string.StateActive,
icon: task.icon.TaskState,
color: PaletteColorIndexes.Blueberry,
defaultStatusName: 'New state',
@@ -431,4 +562,252 @@ export function createModel (builder: Builder): void {
// },
// task.ids.AssigneedNotification
// )
builder.mixin(task.mixin.TaskTypeClass, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.TaskTypeClassPresenter
})
builder.mixin(task.mixin.ProjectTypeClass, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.ProjectTypeClassPresenter
})
builder.mixin(task.class.Task, core.class.Class, setting.mixin.Editable, {
value: true
})
builder.mixin(task.class.Project, core.class.Class, setting.mixin.Editable, {
value: true
})
builder.createDoc(
setting.class.SettingsCategory,
core.space.Model,
{
name: 'statuses',
label: task.string.ManageProjects,
icon: task.icon.ManageTemplates,
component: task.component.ManageProjectsContent,
extraComponents: {
navigation: task.component.ManageProjects,
tools: task.component.ManageProjectsTools
},
group: 'settings-editor',
secured: false,
order: 6000,
expandable: true
},
task.ids.ManageProjects
)
}
/**
* @public
*/
export type FixTaskData = Omit<Data<TaskType>, 'space' | 'statuses' | 'parent'> & { _id?: TaskType['_id'] }
export interface FixTaskResult {
taskTypes: TaskType[]
projectTypes: ProjectType[]
projects: Project[]
}
/**
* @public
*/
export async function fixTaskTypes (
client: MigrationClient,
descriptor: Ref<ProjectTypeDescriptor>,
dataFactory: (t: ProjectType) => Promise<FixTaskData[]>
): Promise<FixTaskResult> {
const categoryObj = client.model.findObject(descriptor)
if (categoryObj === undefined) {
throw new Error('category is not found in model')
}
const projectTypes = await client.find<ProjectType>(DOMAIN_SPACE, {
_class: task.class.ProjectType,
descriptor
})
const baseClassClass = client.hierarchy.getClass(categoryObj.baseClass)
const resultTaskTypes: TaskType[] = []
const resultProjects: Project[] = []
for (const t of projectTypes) {
t.tasks = [...(t.tasks ?? [])]
if (t.targetClass === undefined) {
const targetProjectClassId: Ref<Class<Doc>> = generateId()
t.targetClass = targetProjectClassId
await client.create<TxCreateDoc<Doc>>(DOMAIN_TX, {
_id: generateId(),
objectId: targetProjectClassId,
_class: core.class.TxCreateDoc,
objectClass: core.class.Class,
objectSpace: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
space: core.space.Model,
attributes: {
extends: categoryObj.baseClass,
kind: ClassifierKind.MIXIN,
label: baseClassClass.label,
icon: baseClassClass.icon
}
})
await client.create<TxMixin<Class<ProjectType>, ProjectTypeClass>>(DOMAIN_TX, {
_class: core.class.TxMixin,
_id: generateId(),
space: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
objectId: targetProjectClassId,
objectClass: core.class.Class,
objectSpace: core.space.Model,
mixin: task.mixin.ProjectTypeClass,
attributes: {
projectType: t._id
}
})
await client.update(
DOMAIN_SPACE,
{
_id: t._id
},
{ $set: { targetClass: targetProjectClassId } }
)
}
const dataTypes = await dataFactory(t)
const projects = await client.find<Project>(DOMAIN_SPACE, { type: t._id })
resultProjects.push(...projects)
for (const data of dataTypes) {
const taskTypeId: Ref<TaskType> = data._id ?? generateId()
const descr = client.model.getObject(data.descriptor)
const statuses = await client.find<Status>(DOMAIN_STATUS, {
_id: { $in: t.statuses.map((it) => it._id) },
_class: data.statusClass
})
const dStatuses = [...t.statuses.map((it) => it._id)]
const statusAttr = findStatusAttr(client.hierarchy, data.ofClass)
// Ensure we have at leas't one item in every category.
for (const c of data.statusCategories) {
const cat = await client.model.findOne(core.class.StatusCategory, { _id: c })
const st = statuses.find((it) => it.category === c)
if (st === undefined) {
// We need to add new status into missing category
const statusId: Ref<Status> = generateId()
await client.create<Status>(DOMAIN_STATUS, {
_id: statusId,
_class: data.statusClass,
category: c,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
name: cat?.defaultStatusName ?? 'New state',
space: task.space.Statuses,
ofAttribute: statusAttr._id
})
dStatuses.push(statusId)
await client.update(
DOMAIN_SPACE,
{
_id: t._id
},
{ $push: { statuses: { _id: statusId } } }
)
t.statuses.push({ _id: statusId, taskType: taskTypeId })
}
}
const taskType: TaskType = {
...data,
parent: t._id,
_id: taskTypeId,
_class: task.class.TaskType,
space: t._id,
statuses: dStatuses,
modifiedBy: core.account.System,
modifiedOn: Date.now(),
kind: 'both',
icon: data.icon ?? descr.icon
}
const ofClassClass = client.hierarchy.getClass(data.ofClass)
taskType.icon = ofClassClass.icon
// Create target class for custom field.
const targetClassId: Ref<Class<Doc>> = generateId()
taskType.targetClass = targetClassId
await client.create<TxCreateDoc<Doc>>(DOMAIN_TX, {
_id: generateId(),
objectId: targetClassId,
_class: core.class.TxCreateDoc,
objectClass: core.class.Class,
objectSpace: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
space: core.space.Model,
attributes: {
extends: data.ofClass,
kind: ClassifierKind.MIXIN,
label: getEmbeddedLabel(data.name),
icon: ofClassClass.icon
}
})
await client.create<TxMixin<Class<TaskType>, TaskTypeClass>>(DOMAIN_TX, {
_class: core.class.TxMixin,
_id: generateId(),
space: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
objectId: targetClassId,
objectClass: core.class.Class,
objectSpace: core.space.Model,
mixin: task.mixin.TaskTypeClass,
attributes: {
taskType: taskTypeId,
projectType: t._id
}
})
await client.create(DOMAIN_TASK, taskType)
resultTaskTypes.push(taskType)
await client.update(
DOMAIN_SPACE,
{
_id: t._id
},
{ $push: { tasks: taskTypeId } }
)
t.tasks.push(taskTypeId)
// Update kind and target classId
await client.update(
DOMAIN_TASK,
{ space: { $in: projects.map((it) => it._id) }, _class: data.ofClass },
{ $set: { kind: taskTypeId } }
)
}
// We need to fix project statuses field, for proper icon calculation.
}
for (const t of projectTypes) {
const ttypes = await client.find<TaskType>(DOMAIN_TASK, { _id: { $in: t.tasks } })
const newStatuses = calculateStatuses(t, new Map(ttypes.map((it) => [it._id, it])), [])
await client.update(
DOMAIN_SPACE,
{ _id: t._id },
{
$set: {
statuses: newStatuses
}
}
)
}
return { taskTypes: resultTaskTypes, projectTypes, projects: resultProjects }
}
+7 -539
View File
@@ -13,46 +13,20 @@
// limitations under the License.
//
import { TxOperations, type Class, type Doc, type Ref } from '@hcengineering/core'
import {
type Attribute,
type Class,
DOMAIN_STATUS,
DOMAIN_TX,
type Data,
type Doc,
type Ref,
type Space,
type Status,
TxOperations,
generateId,
toIdMap
} from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate,
tryMigrate,
tryUpgrade
tryUpgrade,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import tags from '@hcengineering/model-tags'
import {
type Project,
type ProjectStatus,
type ProjectType,
type ProjectTypeCategory,
type Task,
createState,
taskId
} from '@hcengineering/task'
import view, { type Filter } from '@hcengineering/view'
import { DOMAIN_KANBAN, DOMAIN_TASK } from '.'
import { taskId } from '@hcengineering/task'
import task from './plugin'
type ProjectData = Omit<Data<ProjectType>, 'statuses' | 'private' | 'members' | 'archived'>
type OldStatus = Status & { rank: string }
/**
* @public
*/
@@ -65,47 +39,6 @@ export async function createSequence (tx: TxOperations, _class: Ref<Class<Doc>>)
}
}
/**
* @public
*/
export async function createProjectType (
client: TxOperations,
data: ProjectData,
states: Data<Status>[],
_id: Ref<ProjectType>,
stateClass: Ref<Class<Status>> = core.class.Status
): Promise<Ref<ProjectType>> {
const current = await client.findOne(task.class.ProjectType, { _id })
if (current !== undefined) {
return current._id
}
const statuses: Ref<Status>[] = []
for (const st of states) {
statuses.push(await createState(client, stateClass, st))
}
const tmpl = await client.createDoc(
task.class.ProjectType,
core.space.Model,
{
description: data.description,
shortDescription: data.shortDescription,
category: data.category,
statuses: statuses.map((p) => {
return { _id: p }
}),
name: data.name,
private: false,
members: [],
archived: false
},
_id
)
return tmpl
}
async function createDefaultSequence (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: task.space.Sequence
@@ -151,469 +84,9 @@ async function createDefaults (tx: TxOperations): Promise<void> {
await createDefaultStatesSpace(tx)
}
async function renameState (client: MigrationClient): Promise<void> {
const toUpdate = await client.find(DOMAIN_TASK, { state: { $exists: true } })
if (toUpdate.length > 0) {
for (const doc of toUpdate) {
await client.update(
DOMAIN_TX,
{ objectId: doc._id },
{ $rename: { 'attributes.state': 'attributes.status', 'operations.state': 'operations.status' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.objectId': doc._id },
{ $rename: { 'tx.attributes.state': 'tx.attributes.status', 'tx.operations.state': 'tx.operations.status' } }
)
}
await client.update(DOMAIN_TASK, { _id: { $in: toUpdate.map((p) => p._id) } }, { $rename: { state: 'status' } })
}
}
async function renameStatePrefs (client: MigrationUpgradeClient): Promise<void> {
const txop = new TxOperations(client, core.account.System)
const prefs = await client.findAll(view.class.ViewletPreference, {})
for (const pref of prefs) {
let update = false
const config = pref.config
for (let index = 0; index < config.length; index++) {
const conf = config[index]
if (typeof conf === 'string') {
if (conf === 'state') {
config[index] = 'status'
update = true
} else if (conf === '$lookup.state') {
config[index] = '$lookup.status'
update = true
}
} else if (conf.key === 'state') {
conf.key = 'status'
update = true
}
}
if (update) {
await txop.update(pref, {
config
})
}
}
const res = await client.findAll(view.class.FilteredView, { filters: /"key":"state"/ as any })
if (res.length > 0) {
for (const doc of res) {
const filters = JSON.parse(doc.filters) as Filter[]
for (const filter of filters) {
if (filter.key.key === 'state') {
filter.key.key = 'status'
}
}
await txop.update(doc, {
filters: JSON.stringify(filters)
})
}
}
}
async function fixStatusAttributes (client: MigrationClient): Promise<void> {
const spaces = await client.find<Space>(DOMAIN_SPACE, {})
const map = toIdMap(spaces)
const oldStatuses = await client.find<OldStatus>(DOMAIN_STATUS, { ofAttribute: { $exists: false } })
for (const oldStatus of oldStatuses) {
const space = map.get(oldStatus.space)
if (space !== undefined) {
try {
let ofAttribute = task.attribute.State
if (space._class === ('recruit:class:Vacancy' as Ref<Class<Space>>)) {
ofAttribute = 'recruit:attribute:State' as Ref<Attribute<Status>>
}
if (space._class === ('lead:class:Funnel' as Ref<Class<Space>>)) {
ofAttribute = 'lead:attribute:State' as Ref<Attribute<Status>>
}
if (space._class === ('board:class:Board' as Ref<Class<Space>>)) {
ofAttribute = 'board:attribute:State' as Ref<Attribute<Status>>
}
if (space._class === ('tracker:class:Project' as Ref<Class<Space>>)) {
ofAttribute = 'tracker:attribute:IssueStatus' as Ref<Attribute<Status>>
}
if (ofAttribute !== oldStatus.ofAttribute) {
await client.update(DOMAIN_STATUS, { _id: oldStatus._id }, { ofAttribute })
}
} catch (err) {
console.log(err)
}
}
}
}
function getTemplateOfAttribute (space: Ref<Space>): Ref<Attribute<Status>> {
let ofAttribute = task.attribute.State
if (space === ('recruit:space:VacancyTemplates' as Ref<Space>)) {
ofAttribute = 'recruit:attribute:State' as Ref<Attribute<Status>>
}
if (space === ('lead:space:FunnelTemplates' as Ref<Space>)) {
ofAttribute = 'lead:attribute:State' as Ref<Attribute<Status>>
}
if (space === ('board:space:BoardTemplates' as Ref<Space>)) {
ofAttribute = 'board:attribute:State' as Ref<Attribute<Status>>
}
return ofAttribute
}
async function fixStatusDoneAttributes (client: MigrationClient): Promise<void> {
const oldStatuses = await client.find<OldStatus>(DOMAIN_STATUS, {})
for (const oldStatus of oldStatuses) {
if (!oldStatus.ofAttribute.includes('DoneState')) continue
const ofAttribute = oldStatus.ofAttribute.replace('DoneState', 'State')
await client.update(DOMAIN_STATUS, { _id: oldStatus._id }, { ofAttribute })
}
}
async function removeDoneStatuses (client: MigrationClient): Promise<void> {
const tasks = await client.find<Task>(DOMAIN_TASK, { doneState: { $exists: true } })
for (const task of tasks) {
if ((task as any).doneState != null) {
await client.update(DOMAIN_TASK, { _id: task._id }, { status: (task as any).doneState, isDone: true })
}
await client.update(DOMAIN_TASK, { _id: task._id }, { $unset: { doneState: '' } })
}
await client.update(
DOMAIN_TX,
{ 'tx.operations.doneState': { $ne: null } },
{ $rename: { 'tx.operations.doneState': 'tx.operations.status' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.attributes.doneState': { $ne: null } },
{ $rename: { 'tx.attributes.doneState': 'tx.attributes.status' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.operations.doneState': { $exists: true } },
{ $unset: { 'tx.operations.doneState': '' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.attributes.doneState': { $exists: true } },
{ $unset: { 'tx.attributes.doneState': '' } }
)
// we need join doneStates to states for all projects
const classes = client.hierarchy.getDescendants(task.class.Project)
const projects = await client.find<Project>(DOMAIN_SPACE, { _class: { $in: classes }, doneStates: { $exists: true } })
for (const project of projects) {
await client.update(
DOMAIN_SPACE,
{ _id: project._id },
{ states: (project as any).states.concat((project as any).doneStates) }
)
await client.update(DOMAIN_SPACE, { _id: project._id }, { $unset: { doneStates: '' } })
}
}
async function removeStateClass (client: MigrationClient): Promise<void> {
await client.update<Status>(
DOMAIN_STATUS,
{ _class: 'task:class:State' as Ref<Class<Doc>> },
{ _class: core.class.Status, category: task.statusCategory.Active }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:State' }, { objectClass: core.class.Status })
await client.update(
DOMAIN_STATUS,
{ _class: 'task:class:WonState' as Ref<Class<Doc>> },
{ _class: core.class.Status, category: task.statusCategory.Won }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:WonState' }, { objectClass: core.class.Status })
await client.update(
DOMAIN_STATUS,
{ _class: 'task:class:LostState' as Ref<Class<Doc>> },
{ _class: core.class.Status, category: task.statusCategory.Lost }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:LostState' }, { objectClass: core.class.Status })
await client.update(
DOMAIN_STATUS,
{ _class: 'task:class:DoneState' as Ref<Class<Doc>> },
{ _class: core.class.Status }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:DoneState' }, { objectClass: core.class.Status })
}
async function migrateTemplatesToTypes (client: MigrationClient): Promise<void> {
interface KanbanTemplate extends Doc {
title: string
description?: string
shortDescription?: string
}
interface KanbanTemplateSpace extends Space {
attachedToClass: Ref<Class<Doc>>
}
interface StateTemplate extends Doc, Status {
attachedTo: Ref<KanbanTemplate>
rank: string
}
const classes = client.hierarchy.getDescendants(task.class.Project)
const templates = await client.find<KanbanTemplate>(DOMAIN_KANBAN, {
_class: 'task:class:KanbanTemplate' as Ref<Class<Doc>>
})
for (const template of templates) {
const used = await client.find(DOMAIN_SPACE, { templateId: template._id })
if (used.length === 0) {
await client.delete(DOMAIN_KANBAN, template._id)
continue
}
const space = (
await client.find<KanbanTemplateSpace>(DOMAIN_SPACE, { _id: template.space as Ref<KanbanTemplateSpace> })
)[0]
if (space === undefined) continue
const states = await client.find<StateTemplate>(
DOMAIN_KANBAN,
{ _class: 'task:class:StateTemplate' as Ref<Class<Doc>>, attachedTo: template._id },
{ sort: { rank: 1 } }
)
const wonStates = await client.find<StateTemplate>(
DOMAIN_KANBAN,
{ _class: 'task:class:WonStateTemplate' as Ref<Class<Doc>>, attachedTo: template._id },
{ sort: { rank: 1 } }
)
const lostStates = await client.find<StateTemplate>(
DOMAIN_KANBAN,
{ _class: 'task:class:LostStateTemplate' as Ref<Class<Doc>>, attachedTo: template._id },
{ sort: { rank: 1 } }
)
const statuses: ProjectStatus[] = []
const currentStates = await client.find<Status>(DOMAIN_STATUS, {})
for (const st of states) {
const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase())
if (exists !== undefined) {
statuses.push({ _id: exists._id, color: st.color })
} else {
const id = generateId<Status>()
await client.create<Status>(DOMAIN_STATUS, {
ofAttribute: getTemplateOfAttribute(st.space),
name: st.name,
_id: id,
space: task.space.Statuses,
modifiedOn: st.modifiedOn,
modifiedBy: st.modifiedBy,
_class: core.class.Status,
color: st.color ?? 9,
createdBy: st.createdBy,
createdOn: st.createdOn,
category: task.statusCategory.Active
})
statuses.push({ _id: id, color: st.color })
}
}
for (const st of wonStates) {
const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase())
if (exists !== undefined) {
statuses.push({ _id: exists._id, color: st.color })
} else {
const id = generateId<Status>()
await client.create<Status>(DOMAIN_STATUS, {
ofAttribute: getTemplateOfAttribute(st.space),
name: st.name,
_id: id,
space: task.space.Statuses,
modifiedOn: st.modifiedOn,
modifiedBy: st.modifiedBy,
_class: core.class.Status,
color: st.color ?? 15,
createdBy: st.createdBy,
createdOn: st.createdOn,
category: task.statusCategory.Won
})
statuses.push({ _id: id, color: st.color })
}
}
for (const st of lostStates) {
const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase())
if (exists !== undefined) {
statuses.push({ _id: exists._id, color: st.color })
} else {
const id = generateId<Status>()
await client.create<Status>(DOMAIN_STATUS, {
ofAttribute: getTemplateOfAttribute(st.space),
name: st.name,
_id: id,
space: task.space.Statuses,
modifiedOn: st.modifiedOn,
modifiedBy: st.modifiedBy,
_class: core.class.Status,
color: st.color ?? 0,
createdBy: st.createdBy,
createdOn: st.createdOn,
category: task.statusCategory.Lost
})
statuses.push({ _id: id, color: st.color })
}
}
const category = await getProjectTypeCategory(client, space.attachedToClass)
await client.create<ProjectType>(DOMAIN_SPACE, {
name: template.title,
description: template.description ?? '',
shortDescription: template.shortDescription,
category: category ?? (template.space as Ref<Doc> as Ref<ProjectTypeCategory>),
private: false,
members: [],
archived: false,
_id: template._id as Ref<Doc> as Ref<ProjectType>,
space: core.space.Space,
modifiedOn: template.modifiedOn,
modifiedBy: template.modifiedBy,
_class: task.class.ProjectType,
statuses
})
await client.delete(DOMAIN_KANBAN, template._id)
}
// we should found all projects without types and has templateID we should just rename it to type (if it exists)
const projectsWithTemplate = await client.find<Project>(DOMAIN_SPACE, {
_class: { $in: classes },
type: { $exists: false },
templateId: { $exists: true }
})
for (const project of projectsWithTemplate) {
await client.update(DOMAIN_SPACE, { _id: project._id }, { type: (project as any).templateId })
}
// we should remove all state templates
const stateClasses = [
'task:class:StateTemplate' as Ref<Class<Doc>>,
'task:class:WonStateTemplate' as Ref<Class<Doc>>,
'task:class:LostStateTemplate' as Ref<Class<Doc>>
]
const states = await client.find(DOMAIN_KANBAN, { _class: { $in: stateClasses } })
for (const st of states) {
await client.delete(DOMAIN_KANBAN, st._id)
}
}
async function getProjectTypeCategory (
client: MigrationClient,
_class: Ref<Class<Project>>
): Promise<Ref<ProjectTypeCategory> | undefined> {
let clazz = _class
while (true) {
const res = await client.model.findAll(task.class.ProjectTypeCategory, { attachedToClass: clazz })
if (res[0] !== undefined) return res[0]._id
const parent = client.hierarchy.getClass(clazz)
if (parent.extends === undefined) return
clazz = parent.extends
}
}
async function migrateProjectTypes (client: MigrationClient): Promise<void> {
const classes = client.hierarchy.getDescendants(task.class.Project)
// we should found all projects without types and group by class and then by statuses and create new type
const projects = await client.find<Project>(DOMAIN_SPACE, { _class: { $in: classes }, type: { $exists: false } })
const projectsByCategory = new Map<Ref<ProjectTypeCategory>, Project[]>()
for (const project of projects) {
const category = await getProjectTypeCategory(client, project._class)
if (category === undefined) continue
const arr = projectsByCategory.get(category) ?? []
arr.push(project)
projectsByCategory.set(category, arr)
}
for (const [category, projects] of projectsByCategory) {
const gouped = group(projects)
for (const gr of gouped) {
await client.create<ProjectType>(DOMAIN_SPACE, {
category,
name: gr.projects[0].name,
description: '',
private: false,
members: [],
archived: false,
_id: gr.type,
space: core.space.Space,
modifiedOn: Date.now(),
modifiedBy: core.account.System,
_class: task.class.ProjectType,
statuses: gr.statuses.map((p) => {
return { _id: p }
})
})
await client.update(DOMAIN_SPACE, { _id: { $in: gr.projects.map((p) => p._id) } }, { type: gr.type })
}
}
// we need remove states from all projects
const allProjects = await client.find<Project>(DOMAIN_SPACE, { _class: { $in: classes } })
await Promise.all(
allProjects.map(async (project) => {
await client.update(
DOMAIN_SPACE,
{ _id: project._id },
{ $unset: { states: '', templateId: '', doneStates: '' } }
)
})
)
}
interface ProjectTypeGroup {
statuses: Ref<Status>[]
projects: Project[]
type: Ref<ProjectType>
}
function group (projects: Project[]): ProjectTypeGroup[] {
const map = new Map<string, ProjectTypeGroup>()
for (const project of projects) {
const ids = getIds((project as any).states ?? [])
const obj: ProjectTypeGroup = map.get(ids) ?? {
statuses: (project as any).states ?? [],
projects: [],
type: generateId<ProjectType>()
}
obj.projects.push(project)
map.set(ids, obj)
}
return Array.from(map.values())
}
function getIds (states: Ref<Status>[]): string {
return states.join(',')
}
export const taskOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, taskId, [
{
state: 'fixStatusAttributes',
func: fixStatusAttributes
},
{
state: 'renameState',
func: renameState
},
{
state: 'removeDoneStatuses',
func: removeDoneStatuses
},
{
state: 'fixStatusDoneAttributes',
func: fixStatusDoneAttributes
},
{
state: 'removeStateClass',
func: removeStateClass
},
{
state: 'migrateTemplatesToTypes',
func: migrateTemplatesToTypes
},
{
state: 'migrateProjectTypes',
func: migrateProjectTypes
},
{
state: 'projectTypeSpace',
func: async (client) => {
@@ -640,11 +113,6 @@ export const taskOperation: MigrateOperation = {
task.category.TaskTag
)
await tryUpgrade(client, taskId, [
{
state: 'renameStatePrefs',
func: renameStatePrefs
}
])
await tryUpgrade(client, taskId, [])
}
}
+15 -2
View File
@@ -14,8 +14,9 @@
// limitations under the License.
//
import {} from '@hcengineering/notification'
import type { Ref, Space } from '@hcengineering/core'
import { mergeIds } from '@hcengineering/platform'
import { mergeIds, type IntlString } from '@hcengineering/platform'
import { type TagCategory } from '@hcengineering/tags'
import { taskId } from '@hcengineering/task'
import task from '@hcengineering/task-resources/src/plugin'
@@ -59,7 +60,14 @@ export default mergeIds(taskId, task, {
StatusSelector: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,
TypesView: '' as AnyComponent,
StateIconPresenter: '' as AnyComponent
StateIconPresenter: '' as AnyComponent,
TaskTypePresenter: '' as AnyComponent,
ProjectTypePresenter: '' as AnyComponent,
TaskTypeClassPresenter: '' as AnyComponent,
ProjectTypeClassPresenter: '' as AnyComponent,
ManageProjects: '' as AnyComponent,
ManageProjectsTools: '' as AnyComponent,
ManageProjectsContent: '' as AnyComponent
},
space: {
TasksPublic: '' as Ref<Space>
@@ -67,5 +75,10 @@ export default mergeIds(taskId, task, {
viewlet: {
TableIssue: '' as Ref<Viewlet>,
KanbanIssue: '' as Ref<Viewlet>
},
string: {
ManageProjects: '' as IntlString,
StateBacklog: '' as IntlString,
StateActive: '' as IntlString
}
})