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
+95 -13
View File
@@ -30,7 +30,8 @@ import {
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
import { Action, ViewletDescriptor } from '@hcengineering/view'
import { Action, IconProps, ViewletDescriptor } from '@hcengineering/view'
import { NotificationType } from '../../notification/lib/index'
/**
* @public
@@ -47,6 +48,7 @@ export interface Project extends Space {
* @public
*/
export interface Task extends AttachedDoc, DocWithRank {
kind: Ref<TaskType>
status: Ref<Status>
isDone?: boolean
number: number
@@ -84,26 +86,96 @@ export interface Sequence extends Doc {
sequence: number
}
export interface ProjectStatus {
export interface ProjectStatus extends IconProps {
_id: Ref<Status>
// Optional color
color?: number
taskType: Ref<TaskType>
}
/**
* @public
*/
export type TaskTypeKind = 'task' | 'subtask' | 'both'
/**
* @public
*/
export interface TaskTypeDescriptor extends Doc {
name: IntlString
description: IntlString
icon: Asset
baseClass: Ref<Class<Task>>
// If specified, will allow to be created by users, system type overwise
allowCreate: boolean
}
/**
* @public
*/
export interface TaskType extends Doc, IconProps {
parent: Ref<ProjectType>
descriptor: Ref<TaskTypeDescriptor>
name: string
kind: TaskTypeKind
// Specify if task is allowed to be used as subtask of following tasks.
allowedAsChildOf?: Ref<TaskType>[]
ofClass: Ref<Class<Task>> // Base class for task
targetClass: Ref<Class<Task>> // Class or Mixin mixin to hold all user defined attributes.
// Allowed statuses and ordering
statuses: Ref<Status>[]
statusClass: Ref<Class<Status>>
statusCategories: Ref<StatusCategory>[]
}
/**
* @public
*
* A a mixin for Class bind to taskType.
*/
export interface TaskTypeClass extends Class<TaskType> {
taskType: Ref<TaskType>
projectType: Ref<ProjectType>
}
/**
* @public
*
* A a mixin for Class bind to taskType.
*/
export interface ProjectTypeClass extends Class<ProjectType> {
projectType: Ref<ProjectType>
}
/**
* @public
*
* Define a user customized project type.
*/
export interface ProjectType extends Space {
shortDescription?: string
category: Ref<ProjectTypeCategory>
descriptor: Ref<ProjectTypeDescriptor>
tasks: Ref<TaskType>[]
// Color and extra options per project type.
// All statuses per project has same color.
statuses: ProjectStatus[]
// A mixin for project
targetClass: Ref<Class<Project>>
}
export interface ProjectTypeCategory extends Doc {
/**
* @public
*/
export interface ProjectTypeDescriptor extends Doc {
name: IntlString
description: IntlString
icon: AnyComponent
editor?: AnyComponent
attachedToClass: Ref<Class<Project>>
statusClass: Ref<Class<Status>>
statusCategories: Ref<StatusCategory>[]
baseClass: Ref<Class<Task>>
}
/**
@@ -141,7 +213,9 @@ const task = plugin(taskId, {
Move: '' as Ref<Action>
},
mixin: {
KanbanCard: '' as Ref<Mixin<KanbanCard>>
KanbanCard: '' as Ref<Mixin<KanbanCard>>,
TaskTypeClass: '' as Ref<Mixin<TaskTypeClass>>,
ProjectTypeClass: '' as Ref<Mixin<ProjectTypeClass>>
},
attribute: {
State: '' as Ref<Attribute<Status>>
@@ -170,15 +244,18 @@ const task = plugin(taskId, {
AssignedToMe: '' as IntlString,
Dashboard: '' as IntlString,
ProjectTypes: '' as IntlString,
TaskType: '' as IntlString,
ProjectType: '' as IntlString
},
class: {
Task: '' as Ref<Class<Task>>,
Sequence: '' as Ref<Class<Sequence>>,
TodoItem: '' as Ref<Class<TodoItem>>,
ProjectTypeDescriptor: '' as Ref<Class<ProjectTypeDescriptor>>,
ProjectType: '' as Ref<Class<ProjectType>>,
ProjectTypeCategory: '' as Ref<Class<ProjectTypeCategory>>,
Project: '' as Ref<Class<Project>>
Project: '' as Ref<Class<Project>>,
TaskTypeDescriptor: '' as Ref<Class<TaskTypeDescriptor>>,
TaskType: '' as Ref<Class<TaskType>>,
Task: '' as Ref<Class<Task>>
},
viewlet: {
Kanban: '' as Ref<ViewletDescriptor>,
@@ -203,6 +280,7 @@ const task = plugin(taskId, {
Statuses: '' as Ref<Space>
},
statusCategory: {
UnStarted: '' as Ref<StatusCategory>,
Active: '' as Ref<StatusCategory>,
Won: '' as Ref<StatusCategory>,
Lost: '' as Ref<StatusCategory>
@@ -213,6 +291,10 @@ const task = plugin(taskId, {
TodoItemsPopup: '' as AnyComponent,
CreateStatePopup: '' as AnyComponent
},
ids: {
AssigneedNotification: '' as Ref<NotificationType>,
ManageProjects: '' as Ref<Doc>
},
extensions: {
ProjectEditorExtension: '' as ComponentExtensionId
}
+237 -5
View File
@@ -13,10 +13,26 @@
// limitations under the License.
//
import { Class, Data, DocumentQuery, IdMap, Ref, Status, TxOperations } from '@hcengineering/core'
import core, {
Class,
Data,
DocumentQuery,
Hierarchy,
IdMap,
Ref,
Status,
StatusCategory,
TxOperations,
type AnyAttribute,
type RefTo,
Doc,
generateId,
ClassifierKind
} from '@hcengineering/core'
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
import task, { Project, ProjectType } from '.'
import task, { Project, ProjectStatus, ProjectType, Task, TaskType } from '.'
import { getEmbeddedLabel } from '@hcengineering/platform'
/**
* @public
@@ -50,16 +66,58 @@ export const calcRank = (prev?: { rank: string }, next?: { rank: string }): stri
/**
* @public
*/
export function getStates (space: Project | undefined, types: IdMap<ProjectType>, statuses: IdMap<Status>): Status[] {
if (space === undefined) return []
export function getProjectTypeStates (
projectType: Ref<ProjectType> | undefined,
types: IdMap<ProjectType>,
statuses: IdMap<Status>
): Status[] {
if (projectType === undefined) return []
return (
(types
.get(space.type)
.get(projectType)
?.statuses?.map((p) => statuses.get(p._id))
?.filter((p) => p !== undefined) as Status[]) ?? []
)
}
/**
* @public
*/
export function getStates (space: Project | undefined, types: IdMap<ProjectType>, statuses: IdMap<Status>): Status[] {
if (space === undefined) return []
return getProjectTypeStates(space.type, types, statuses)
}
/**
* @public
*/
export function getTaskTypeStates (
taskType: Ref<TaskType> | undefined,
types: IdMap<TaskType>,
statuses: IdMap<Status>
): Status[] {
if (taskType === undefined) return []
return (
(types
.get(taskType)
?.statuses?.map((p) => statuses.get(p))
?.filter((p) => p !== undefined) as Status[]) ?? []
)
}
/**
* @public
*/
export function getStatusIndex (type: ProjectType, taskTypes: IdMap<TaskType>, status: Ref<Status>): number {
return (
type.tasks
.map((it) => taskTypes.get(it))
.flatMap((it) => it?.statuses.indexOf(status))
.filter((it) => (it ?? 0) >= 0)
.reduce((p, c) => (p ?? 0) + (c ?? 0), 0) ?? -1
)
}
/**
* @public
*/
@@ -79,3 +137,177 @@ export async function createState<T extends Status> (
const res = await client.createDoc(_class, task.space.Statuses, data)
return res
}
/**
* @public
*/
export function isTaskCategory (category: Ref<StatusCategory>): boolean {
return (
category === task.statusCategory.Active ||
category === task.statusCategory.Active ||
category === task.statusCategory.Won ||
category === task.statusCategory.Lost
)
}
/**
* @public
*/
export function calculateStatuses (
projectType: { statuses: ProjectType['statuses'], tasks: ProjectType['tasks'] },
taskTypes: Map<Ref<TaskType>, Data<TaskType>>,
override: Array<{ taskTypeId: Ref<TaskType>, statuses: Array<Ref<Status>> }>
): ProjectStatus[] {
const stIds = new Map(projectType.statuses.map((p) => [p._id, p]))
const processed = new Set<string>()
const result: ProjectStatus[] = []
for (const tt of projectType.tasks) {
const statusesList = override.find((it) => it.taskTypeId === tt)?.statuses ?? taskTypes.get(tt)?.statuses ?? []
for (const tts of statusesList) {
const prjStatus = stIds.get(tts)
const key = `${tts}:${tt}`
if (!processed.has(key)) {
processed.add(key)
result.push({ ...(prjStatus ?? {}), _id: tts, taskType: tt })
}
}
}
return result
}
/**
* @public
*/
export function findStatusAttr (h: Hierarchy, _class: Ref<Class<Task>>): AnyAttribute {
const attrs = h.getAllAttributes(_class)
for (const it of attrs.values()) {
if (it.type._class === core.class.RefTo && h.isDerived((it.type as RefTo<any>).to, core.class.Status)) {
return it
}
}
return h.getAttribute(task.class.Task, 'status')
}
export type TaskTypeWithFactory = Omit<Data<TaskType>, 'statuses' | 'parent' | 'targetClass'> & {
_id: TaskType['_id']
factory: Data<Status>[]
} & Partial<Pick<TaskType, 'targetClass'>>
type ProjectData = Omit<Data<ProjectType>, 'statuses' | 'private' | 'members' | 'archived' | 'targetClass'>
/**
* @public
*/
export async function createProjectType (
client: TxOperations,
data: ProjectData,
tasks: TaskTypeWithFactory[],
_id: Ref<ProjectType>
): Promise<Ref<ProjectType>> {
const current = await client.findOne(task.class.ProjectType, { _id })
if (current !== undefined) {
return current._id
}
async function createStates (states: Data<Status>[], stateClass: Ref<Class<Status>>): Promise<Ref<Status>[]> {
const statuses: Ref<Status>[] = []
for (const st of states) {
statuses.push(await createState(client, stateClass, st))
}
return statuses
}
const _tasks: Ref<TaskType>[] = []
const tasksData = new Map<Ref<TaskType>, Data<TaskType>>()
const _statues = new Set<Ref<Status>>()
for (const it of tasks) {
const { factory, _id: taskId, ...data } = it
const statuses = await createStates(factory, data.statusClass)
for (const st of statuses) {
_statues.add(st)
}
const tdata = {
...data,
parent: _id,
statuses
}
const ofClassClass = client.getHierarchy().getClass(data.ofClass)
tdata.icon = ofClassClass.icon
if (tdata.targetClass === undefined) {
// Create target class for custom field.
const targetClassId: Ref<Class<Task>> = generateId()
tdata.targetClass = targetClassId
await client.createDoc(
core.class.Mixin,
core.space.Model,
{
extends: data.ofClass,
kind: ClassifierKind.MIXIN,
label: ofClassClass.label,
icon: ofClassClass.icon
},
targetClassId
)
await client.createMixin(targetClassId, core.class.Mixin, core.space.Model, task.mixin.TaskTypeClass, {
taskType: taskId,
projectType: _id
})
}
await client.createDoc(task.class.TaskType, core.space.Model, tdata as Data<TaskType>, taskId)
tasksData.set(taskId, tdata as Data<TaskType>)
_tasks.push(taskId)
}
const categoryObj = client.getModel().findObject(data.descriptor)
if (categoryObj === undefined) {
throw new Error('category is not found in model')
}
const baseClassClass = client.getHierarchy().getClass(categoryObj.baseClass)
const targetProjectClassId: Ref<Class<Doc>> = generateId()
await client.createDoc(
core.class.Mixin,
core.space.Model,
{
extends: categoryObj.baseClass,
kind: ClassifierKind.MIXIN,
label: getEmbeddedLabel(data.name),
icon: baseClassClass.icon
},
targetProjectClassId,
undefined,
core.account.ConfigUser
)
await client.createMixin(targetProjectClassId, core.class.Mixin, core.space.Model, task.mixin.ProjectTypeClass, {
projectType: _id
})
const tmpl = await client.createDoc(
task.class.ProjectType,
core.space.Model,
{
description: data.description,
shortDescription: data.shortDescription,
descriptor: data.descriptor,
tasks: _tasks,
name: data.name,
private: false,
members: [],
archived: false,
statuses: calculateStatuses({ tasks: _tasks, statuses: [] }, tasksData, []),
targetClass: targetProjectClassId
},
_id
)
return tmpl
}