UBERF-4810 (#4306)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-01-04 23:54:58 +06:00 committed by GitHub
parent 4defb38ff5
commit 6097575d65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 261 additions and 32 deletions

View File

@ -61,7 +61,7 @@ import view, {
template,
actionTemplates as viewTemplates
} from '@hcengineering/model-view'
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import tags from '@hcengineering/tags'
import {
@ -171,6 +171,7 @@ export class TTaskTypeDescriptor extends TDoc implements TaskTypeDescriptor {
// If specified, will allow to be created by users, system type overwize
allowCreate!: boolean
statusCategoriesFunc?: Resource<(project: ProjectType) => Ref<StatusCategory>[]>
}
@Mixin(task.mixin.TaskTypeClass, core.class.Class)
@ -507,12 +508,26 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
ofAttribute: task.attribute.State,
label: task.string.StateActive,
label: task.string.StateUnstarted,
icon: task.icon.TaskState,
color: PaletteColorIndexes.Porpoise,
defaultStatusName: 'New state',
defaultStatusName: 'Todo',
order: 1
},
task.statusCategory.ToDo
)
builder.createDoc(
core.class.StatusCategory,
core.space.Model,
{
ofAttribute: task.attribute.State,
label: task.string.StateActive,
icon: task.icon.TaskState,
color: PaletteColorIndexes.Cerulean,
defaultStatusName: 'New state',
order: 2
},
task.statusCategory.Active
)
@ -525,7 +540,7 @@ export function createModel (builder: Builder): void {
icon: task.icon.TaskState,
color: PaletteColorIndexes.Grass,
defaultStatusName: 'Won',
order: 2
order: 3
},
task.statusCategory.Won
)
@ -539,7 +554,7 @@ export function createModel (builder: Builder): void {
icon: task.icon.TaskState,
color: PaletteColorIndexes.Coin,
defaultStatusName: 'Lost',
order: 3
order: 4
},
task.statusCategory.Lost
)

View File

@ -44,6 +44,7 @@ async function reorderStates (_client: MigrationUpgradeClient): Promise<void> {
const states = toIdMap(await client.findAll(core.class.Status, {}))
const order = [
task.statusCategory.UnStarted,
task.statusCategory.ToDo,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost

View File

@ -79,6 +79,7 @@ export default mergeIds(taskId, task, {
string: {
ManageProjects: '' as IntlString,
StateBacklog: '' as IntlString,
StateActive: '' as IntlString
StateActive: '' as IntlString,
StateUnstarted: '' as IntlString
}
})

View File

@ -663,7 +663,8 @@ export function createModel (builder: Builder): void {
allowCreate: true,
description: tracker.string.Issue,
icon: tracker.icon.Issue,
name: tracker.string.Issue
name: tracker.string.Issue,
statusCategoriesFunc: tracker.function.GetIssueStatusCategories
},
tracker.descriptors.Issue
)

View File

@ -20,7 +20,12 @@ import core, {
generateId,
type Data,
type Ref,
type Status
type Status,
toIdMap,
DOMAIN_TX,
type TxCreateDoc,
TxProcessor,
type Tx
} from '@hcengineering/core'
import {
createOrUpdate,
@ -34,12 +39,14 @@ import { DOMAIN_TASK, createProjectType, fixTaskTypes } from '@hcengineering/mod
import tags from '@hcengineering/tags'
import task, { type ProjectType, type TaskType } from '@hcengineering/task'
import {
type IssueStatus,
TimeReportDayType,
baseIssueTaskStatuses,
classicIssueTaskStatuses,
createStatesData
} from '@hcengineering/tracker'
import tracker from './plugin'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
async function createDefaultProject (tx: TxOperations): Promise<void> {
const current = await tx.findOne(tracker.class.Project, {
@ -132,7 +139,7 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
defaultIssueStatus: state._id,
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay,
defaultAssignee: undefined,
type: tracker.ids.DefaultProjectType
type: tracker.ids.ClassingProjectType
},
tracker.project.DefaultProject
)
@ -176,6 +183,156 @@ async function fixTrackerTaskTypes (client: MigrationClient): Promise<void> {
})
}
async function tryCreateStatus (client: MigrationClient): Promise<Ref<Status>> {
const exists = await client.find<Status>(DOMAIN_STATUS, {
_class: tracker.class.IssueStatus,
name: 'Todo',
ofAttribute: tracker.attribute.IssueStatus
})
if (exists.length > 0) return exists[0]._id
const newStatus: IssueStatus = {
ofAttribute: tracker.attribute.IssueStatus,
name: 'Todo',
_id: generateId(),
category: task.statusCategory.ToDo,
color: PaletteColorIndexes.Porpoise,
space: task.space.Statuses,
modifiedOn: Date.now(),
createdBy: core.account.System,
createdOn: Date.now(),
modifiedBy: core.account.System,
_class: tracker.class.IssueStatus
}
await client.create<Status>(DOMAIN_STATUS, newStatus)
const tx: TxCreateDoc<IssueStatus> = {
modifiedOn: Date.now(),
createdBy: core.account.System,
createdOn: Date.now(),
modifiedBy: core.account.System,
_id: generateId(),
objectClass: newStatus._class,
objectSpace: newStatus.space,
objectId: newStatus._id,
_class: core.class.TxCreateDoc,
space: core.space.Tx,
attributes: {
category: newStatus.category,
color: newStatus.color,
ofAttribute: newStatus.ofAttribute,
name: newStatus.name
}
}
await client.create(DOMAIN_TX, tx)
return newStatus._id
}
async function restoreToDoCategory (client: MigrationClient): Promise<void> {
const updatedStatus = new Set<Ref<Status>>()
const allStatuses = await client.find<Status>(
DOMAIN_STATUS,
{ _class: tracker.class.IssueStatus },
{ projection: { name: 1, _id: 1 } }
)
const statusMap = toIdMap(allStatuses)
const projects = await client.find<ProjectType>(DOMAIN_SPACE, {
_class: task.class.ProjectType,
descriptor: tracker.descriptors.ProjectType,
classic: true
})
for (const p of projects) {
const changed = new Map<Ref<Status>, Ref<Status>>()
const pushStatuses: {
_id: Ref<Status>
taskType: Ref<TaskType>
}[] = []
const taskTypes = await client.find<TaskType>(DOMAIN_TASK, {
_class: task.class.TaskType,
descriptor: tracker.descriptors.Issue,
_id: { $in: p.tasks }
})
for (const taskType of taskTypes) {
if (taskType.statusCategories.includes(task.statusCategory.ToDo)) continue
const activeIndexes: number[] = []
for (let index = 0; index < taskType.statuses.length; index++) {
const status = taskType.statuses[index]
const st = statusMap.get(status)
if (st === undefined) continue
if (st.category !== task.statusCategory.Active) continue
activeIndexes.push(index)
}
if (activeIndexes.length < 2) {
// we should create new status
const newStatus = await tryCreateStatus(client)
pushStatuses.push({
_id: newStatus,
taskType: taskType._id
})
taskType.statuses.splice(activeIndexes[0] ?? 0, 0, newStatus)
} else {
// let's try to find ToDo status
let changed = false
for (const index of activeIndexes) {
const status = taskType.statuses[index]
const st = statusMap.get(status)
if (st === undefined) continue
const ownTxes = await client.find<Tx>(DOMAIN_TX, { objectId: status })
const attachedTxes = await client.find<Tx>(DOMAIN_TX, { 'tx.objectId': status })
const original = TxProcessor.buildDoc2Doc<Status>([...ownTxes, ...attachedTxes])
if (original === undefined) continue
if (original.category === tracker.issueStatusCategory.Unstarted) {
// We need to update status
if (!updatedStatus.has(status)) {
await client.update<Status>(
DOMAIN_STATUS,
{ _id: status },
{ $set: { category: task.statusCategory.ToDo } }
)
updatedStatus.add(status)
}
changed = true
}
}
if (!changed) {
// we should create new status
const newStatus = await tryCreateStatus(client)
pushStatuses.push({
_id: newStatus,
taskType: taskType._id
})
taskType.statuses.splice(activeIndexes[0] ?? 0, 0, newStatus)
}
}
await client.update(
DOMAIN_TASK,
{ _id: taskType._id },
{
$set: {
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.ToDo,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
],
statuses: taskType.statuses
}
}
)
}
if (changed.size > 0) {
const statuses = p.statuses
.map((it) => {
return {
...it,
_id: changed.get(it._id) ?? it._id
}
})
.concat(pushStatuses)
await client.update(DOMAIN_SPACE, { _id: p._id }, { $set: { statuses } })
}
}
}
export const trackerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, 'tracker', [
@ -205,7 +362,7 @@ export const trackerOperation: MigrateOperation = {
await client.update<Status>(
DOMAIN_STATUS,
{ _class: tracker.class.IssueStatus, category: tracker.issueStatusCategory.Unstarted },
{ $set: { category: task.statusCategory.Active } }
{ $set: { category: task.statusCategory.ToDo } }
)
await client.update<Status>(
DOMAIN_STATUS,
@ -238,6 +395,7 @@ export const trackerOperation: MigrateOperation = {
// We need to replace category
tt.statusCategories = [
task.statusCategory.UnStarted,
task.statusCategory.ToDo,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
@ -250,6 +408,7 @@ export const trackerOperation: MigrateOperation = {
const toRemove: Ref<Status>[] = []
for (const c of [
task.statusCategory.UnStarted,
task.statusCategory.ToDo,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
@ -301,6 +460,10 @@ export const trackerOperation: MigrateOperation = {
{
state: 'fixTaskTypes',
func: fixTrackerTaskTypes
},
{
state: 'restoreToDoCategory',
func: restoreToDoCategory
}
])
},

View File

@ -55,6 +55,7 @@
"DoneStatesWon": "Done status / Won",
"DoneStatesLost": "Done status / Lost",
"StateBacklog": "Backlog",
"StateUnstarted": "Unstarted",
"StateActive": "Active",
"AllStates": "All states",
"DoneStates": "Done states",

View File

@ -55,6 +55,7 @@
"DoneStatesWon": "Завершено / Выиграно",
"DoneStatesLost": "Завершено / Потеряно",
"StateBacklog": "Пул задач",
"StateUnstarted": "Не запущенные",
"StateActive": "Активные",
"AllStates": "Все статусы",
"DoneStates": "Завершенные статусы",

View File

@ -31,6 +31,7 @@
import { createEventDispatcher, onMount } from 'svelte'
import { typeStore } from '../..'
import IconBacklog from '../icons/IconBacklog.svelte'
import IconUnstarted from '../icons/IconUnstarted.svelte'
import IconCanceled from '../icons/IconCanceled.svelte'
import IconCompleted from '../icons/IconCompleted.svelte'
import IconStarted from '../icons/IconStarted.svelte'
@ -104,6 +105,7 @@
const categoryIcons = {
[task.statusCategory.UnStarted]: IconBacklog,
[task.statusCategory.ToDo]: IconUnstarted,
[task.statusCategory.Active]: IconStarted,
[task.statusCategory.Won]: IconCompleted,
[task.statusCategory.Lost]: IconCanceled

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import core, { Class, ClassifierKind, Data, Ref, RefTo, Status, generateId, toIdMap } from '@hcengineering/core'
import { Resource, getEmbeddedLabel } from '@hcengineering/platform'
import { Resource, getEmbeddedLabel, getResource } from '@hcengineering/platform'
import presentation, { Card, getClient, hasResource } from '@hcengineering/presentation'
import {
ProjectType,
@ -25,8 +25,7 @@
createState,
findStatusAttr
} from '@hcengineering/task'
import { DropdownIntlItem, EditBox, getColorNumberByText } from '@hcengineering/ui'
import DropdownLabelsIntl from '@hcengineering/ui/src/components/DropdownLabelsIntl.svelte'
import { DropdownIntlItem, DropdownLabelsIntl, EditBox, getColorNumberByText } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
import TaskTypeKindEditor from './TaskTypeKindEditor.svelte'
@ -35,7 +34,7 @@
const client = getClient()
export let type: ProjectType
export let descriptor: ProjectTypeDescriptor
export let taskType: TaskType
export let taskType: TaskType | undefined
function defaultTaskType (type: ProjectType): Data<TaskType> {
return {
@ -82,6 +81,7 @@
const descr = taskTypeDescriptors.find((it) => it._id === taskTypeDescriptor)
if (descr === undefined) return
const ofClass = descr.baseClass
const _taskType = {
kind,
@ -96,11 +96,21 @@
parent: type._id,
icon: descr.icon
}
if (taskType === undefined && descr.statusCategoriesFunc !== undefined) {
const f = await getResource(descr.statusCategoriesFunc)
if (f !== undefined) {
_taskType.statusCategories = f(type)
}
}
const taskTypeId: Ref<TaskType> = taskType?._id ?? generateId()
const categories = toIdMap(await client.findAll(core.class.StatusCategory, { _id: { $in: statusCategories } }))
const categories = toIdMap(
await client.findAll(core.class.StatusCategory, { _id: { $in: _taskType.statusCategories } })
)
const statusAttr =
findStatusAttr(client.getHierarchy(), ofClass) ?? client.getHierarchy().getAttribute(task.class.Task, 'status')
for (const st of statusCategories) {
for (const st of _taskType.statusCategories) {
const std = categories.get(st)
if (std !== undefined) {
const s = await createState(client, _taskType.statusClass, {
@ -111,7 +121,7 @@
_taskType.statuses.push(s)
if (type.statuses.find((it) => it._id === s) === undefined) {
await client.update(type, {
$push: { statuses: { _id: s, color: getColorNumberByText(std.defaultStatusName), taskType: taskTypeId } }
$push: { statuses: { _id: s, taskType: taskTypeId } }
})
}
}

View File

@ -27,7 +27,7 @@ import {
StatusCategory,
Timestamp
} from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
import { Action, IconProps, ViewletDescriptor } from '@hcengineering/view'
@ -107,6 +107,7 @@ export interface TaskTypeDescriptor extends Doc {
// If specified, will allow to be created by users, system type overwise
allowCreate: boolean
statusCategoriesFunc?: Resource<(project: ProjectType) => Ref<StatusCategory>[]>
}
/**
@ -292,6 +293,8 @@ const task = plugin(taskId, {
},
statusCategory: {
UnStarted: '' as Ref<StatusCategory>,
// For classic project type
ToDo: '' as Ref<StatusCategory>,
Active: '' as Ref<StatusCategory>,
Won: '' as Ref<StatusCategory>,
Lost: '' as Ref<StatusCategory>

View File

@ -59,9 +59,13 @@
let activeStatuses: Ref<IssueStatus>[] = []
$: activeStatusQuery.query(tracker.class.IssueStatus, { category: task.statusCategory.Active }, (result) => {
activeStatuses = result.map(({ _id }) => _id)
})
$: activeStatusQuery.query(
tracker.class.IssueStatus,
{ category: { $in: [task.statusCategory.Active, task.statusCategory.ToDo] } },
(result) => {
activeStatuses = result.map(({ _id }) => _id)
}
)
let active: DocumentQuery<Issue>
$: active = { status: { $in: activeStatuses }, ...spaceQuery }

View File

@ -40,9 +40,13 @@
let activeStatuses: Ref<IssueStatus>[] = []
$: activeStatusQuery.query(tracker.class.IssueStatus, { category: task.statusCategory.Active }, (result) => {
activeStatuses = result.map(({ _id }) => _id)
})
$: activeStatusQuery.query(
tracker.class.IssueStatus,
{ category: { $in: [task.statusCategory.Active, task.statusCategory.ToDo] } },
(result) => {
activeStatuses = result.map(({ _id }) => _id)
}
)
let active: DocumentQuery<Issue>
$: active = { status: { $in: activeStatuses }, ...assigned }

View File

@ -123,6 +123,7 @@ import {
getAllMilestones,
getAllPriority,
getComponentTitle,
getIssueStatusCategories,
getMilestoneTitle,
getVisibleFilters,
issuePrioritySort,
@ -536,7 +537,8 @@ export default async (): Promise<Resources> => ({
GetAllComponents: getAllComponents,
GetAllMilestones: getAllMilestones,
GetVisibleFilters: getVisibleFilters,
IsProjectJoined: async (project: Project) => !project.private || project.members.includes(getCurrentAccount()._id)
IsProjectJoined: async (project: Project) => !project.private || project.members.includes(getCurrentAccount()._id),
GetIssueStatusCategories: getIssueStatusCategories
},
actionImpl: {
Move: move,

View File

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type Client, type Doc, type Ref, type Space } from '@hcengineering/core'
import { type StatusCategory, type Client, type Doc, type Ref, type Space } from '@hcengineering/core'
import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { type ProjectType } from '@hcengineering/task'
import tracker, { trackerId, type IssueDraft } from '@hcengineering/tracker'
import { type AnyComponent, type Location } from '@hcengineering/ui'
import {
@ -394,7 +395,8 @@ export default mergeIds(trackerId, tracker, {
GetAllComponents: '' as GetAllValuesFunc,
GetAllMilestones: '' as GetAllValuesFunc,
GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>,
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>,
GetIssueStatusCategories: '' as Resource<(project: ProjectType) => Array<Ref<StatusCategory>>>
},
aggregation: {
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,

View File

@ -281,6 +281,7 @@ export const milestoneTitleMap: Record<MilestoneViewMode, IntlString> = Object.f
*/
export const listIssueStatusOrder = [
task.statusCategory.Active,
task.statusCategory.ToDo,
task.statusCategory.UnStarted,
task.statusCategory.Won,
task.statusCategory.Lost
@ -291,6 +292,7 @@ export const listIssueStatusOrder = [
*/
export const listIssueKanbanStatusOrder = [
task.statusCategory.UnStarted,
task.statusCategory.ToDo,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
@ -690,6 +692,25 @@ export async function getVisibleFilters (filters: KeyFilter[], space?: Ref<Space
return space === undefined ? filters : filters.filter((f) => f.key !== 'space')
}
export function getIssueStatusCategories (project: ProjectType): Array<Ref<StatusCategory>> {
if (project.classic) {
return [
task.statusCategory.UnStarted,
task.statusCategory.ToDo,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
]
} else {
return [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
]
}
}
interface ManualUpdates {
useStatus: boolean
useComponent: boolean

View File

@ -364,12 +364,10 @@ export class ComponentManager extends DocManager {
*/
export const classicIssueTaskStatuses: TaskStatusFactory[] = [
{ category: task.statusCategory.UnStarted, statuses: [['Backlog', PaletteColorIndexes.Cloud]] },
{ category: task.statusCategory.ToDo, statuses: [['Todo', PaletteColorIndexes.Porpoise]] },
{
category: task.statusCategory.Active,
statuses: [
['Todo', PaletteColorIndexes.Porpoise],
['In progress', PaletteColorIndexes.Cerulean]
]
statuses: [['In progress', PaletteColorIndexes.Cerulean]]
},
{ category: task.statusCategory.Won, statuses: [['Done', PaletteColorIndexes.Grass]] },
{ category: task.statusCategory.Lost, statuses: [['Canceled', PaletteColorIndexes.Coin]] }

View File

@ -25,7 +25,7 @@ export const DEFAULT_USER = 'Appleseed John'
export const DEFAULT_STATUSES_ID = new Map([
['Backlog', 'task:statusCategory:UnStarted'],
['Todo', 'task:statusCategory:Active'],
['Todo', 'task:statusCategory:ToDo'],
['In Progress', 'task:statusCategory:Active'],
['Done', 'task:statusCategory:Won'],
['Canceled', 'task:statusCategory:Lost']