mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-26 11:34:58 +02:00
UBERF-4248: Task type (#4042)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
+403
-24
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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, [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user