UBER-1182: Fix github task types support (#4215)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-12-19 13:08:10 +07:00 committed by GitHub
parent 89706eb350
commit 5413031df0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 195 additions and 85 deletions

View File

@ -19,7 +19,7 @@ import { type Doc, type Ref } from '@hcengineering/core'
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
import { type ProjectType, type TaskTypeDescriptor } from '@hcengineering/task'
import { type ProjectType } from '@hcengineering/task'
import { trackerId } from '@hcengineering/tracker'
import tracker from '@hcengineering/tracker-resources/src/plugin'
import type { AnyComponent } from '@hcengineering/ui/src/types'
@ -109,8 +109,5 @@ export default mergeIds(trackerId, tracker, {
DeleteProject: '' as Ref<Action<Doc, Record<string, any>>>,
DeleteProjectClean: '' as Ref<Action<Doc, Record<string, any>>>,
DeleteIssue: '' as Ref<Action<Doc, Record<string, any>>>
},
descriptors: {
Issue: '' as Ref<TaskTypeDescriptor>
}
})

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, Ref } from '@hcengineering/core'
import { getObjectValue, type Class, type Doc, type Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import {
Button,
@ -68,7 +68,7 @@
$: showCategories =
created.length > 0 ||
objects.map((it) => (it as any)[groupBy]).filter((it, index, arr) => arr.indexOf(it) === index).length > 1
objects.map((it) => getObjectValue(groupBy, it)).filter((it, index, arr) => arr.indexOf(it) === index).length > 1
const checkSelected = (item: Doc): void => {
if (selectedElements.has(item._id)) {
@ -148,7 +148,7 @@
if (created.find((it) => it._id === doc._id) !== undefined) {
return '_created'
}
return toAny(doc)[groupBy]
return getObjectValue(groupBy, toAny(doc))
}
</script>

View File

@ -13,7 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import {
getObjectValue,
type Class,
type Doc,
type DocumentQuery,
type FindOptions,
type Ref
} from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
@ -77,8 +84,8 @@
},
(result) => {
result.sort((a, b) => {
const aval: string = `${(a as any)[groupBy]}`
const bval: string = `${(b as any)[groupBy]}`
const aval: string = `${getObjectValue(groupBy, a as any)}`
const bval: string = `${getObjectValue(groupBy, b as any)}`
return aval.localeCompare(bval)
})
if (created.length > 0) {

View File

@ -97,7 +97,7 @@
space: Ref<Project>,
template: IssueTemplateData,
parent: Ref<Issue> = tracker.ids.NoParent
): Promise<Ref<Issue>> {
): Promise<Ref<Issue> | undefined> {
const lastOne = await client.findOne<Issue>(
tracker.class.Issue,
{ space },
@ -114,6 +114,10 @@
)
const project = await client.findOne(tracker.class.Project, { _id: space })
const rank = calcRank(lastOne, undefined)
const taskType = await client.findOne(task.class.TaskType, { ofClass: tracker.class.Issue })
if (taskType === undefined) {
return
}
const resId = await client.addCollection(tracker.class.Issue, space, parent, tracker.class.Issue, 'subIssues', {
title: template.title + ` (${name})`,
description: template.description,
@ -133,7 +137,8 @@
estimation: template.estimation,
reports: 0,
relations: [{ _id: id, _class: recruit.class.Vacancy }],
childInfo: []
childInfo: [],
kind: taskType._id
})
if ((template.labels?.length ?? 0) > 0) {
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: template.labels } })
@ -181,8 +186,10 @@
if (issueTemplates.length > 0) {
for (const issueTemplate of issueTemplates) {
const issue = await saveIssue(id, issueTemplate.space, issueTemplate)
for (const sub of issueTemplate.children) {
await saveIssue(id, issueTemplate.space, sub, issue)
if (issue !== undefined) {
for (const sub of issueTemplate.children) {
await saveIssue(id, issueTemplate.space, sub, issue)
}
}
}
}

View File

@ -16,7 +16,7 @@
import core, { Doc, FindResult, IdMap, Ref, RefTo, Space, Status, toIdMap } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { TaskType } from '@hcengineering/task'
import { ProjectType, TaskType } from '@hcengineering/task'
import ui, {
EditWithIcon,
Icon,
@ -39,7 +39,7 @@
import view from '@hcengineering/view-resources/src/plugin'
import { buildConfigLookup, getPresenter } from '@hcengineering/view-resources/src/utils'
import { createEventDispatcher } from 'svelte'
import { selectedTaskTypeStore, taskTypeStore } from '..'
import { selectedTaskTypeStore, selectedTypeStore, taskTypeStore, typeStore } from '..'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
@ -67,7 +67,9 @@
search: string,
selectedType: Ref<TaskType> | undefined,
typeStore: IdMap<TaskType>,
statusStore: IdMap<Status>
statusStore: IdMap<Status>,
selectedProjectType: Ref<ProjectType> | undefined,
projectTypeStore: IdMap<ProjectType>
): Promise<void> {
await objectsPromise
targets.clear()
@ -85,13 +87,22 @@
values = values.filter((p) => p?.name.includes(search))
}
} else {
const statuses: Status[] = []
let statuses: Status[] = []
const prjStatuses = new Map(
(
(selectedProjectType !== undefined ? projectTypeStore.get(selectedProjectType) : undefined)?.statuses ?? []
).map((p) => [p._id, p])
)
for (const status of statusStore.values()) {
if (hierarchy.isDerived(status._class, targetClass) && status.ofAttribute === filter.key.attribute._id) {
if (prjStatuses.size > 0 && !prjStatuses.has(status._id)) {
continue
}
statuses.push(status)
targets.add(status._id)
}
}
statuses = statuses.filter((it, idx, arr) => arr.findIndex((q) => q._id === it._id) === idx)
values = await sort(statuses)
}
if (targets.has(undefined)) {
@ -164,7 +175,7 @@
const dispatch = createEventDispatcher()
$: if (targetClass != null) {
void getValues(search, $selectedTaskTypeStore, $taskTypeStore, $statusStore.byId)
void getValues(search, $selectedTaskTypeStore, $taskTypeStore, $statusStore.byId, $selectedTypeStore, $typeStore)
}
</script>

View File

@ -15,7 +15,9 @@
import core, {
Class,
ClassifierKind,
Data,
Doc,
DocumentQuery,
Hierarchy,
IdMap,
@ -23,16 +25,14 @@ import core, {
Status,
StatusCategory,
TxOperations,
type AnyAttribute,
type RefTo,
Doc,
generateId,
ClassifierKind
type AnyAttribute,
type RefTo
} from '@hcengineering/core'
import { PlatformError, getEmbeddedLabel, unknownStatus } from '@hcengineering/platform'
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
import task, { Project, ProjectStatus, ProjectType, Task, TaskType } from '.'
import { getEmbeddedLabel } from '@hcengineering/platform'
/**
* @public
@ -196,6 +196,18 @@ export type TaskTypeWithFactory = Omit<Data<TaskType>, 'statuses' | 'parent' | '
type ProjectData = Omit<Data<ProjectType>, 'statuses' | 'private' | 'members' | 'archived' | 'targetClass'>
async function createStates (
client: TxOperations,
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
}
/**
* @public
*/
@ -210,65 +222,17 @@ export async function createProjectType (
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')
}
await createTaskTypes(tasks, _id, client, _statues, tasksData, _tasks, false)
const baseClassClass = client.getHierarchy().getClass(categoryObj.baseClass)
const targetProjectClassId: Ref<Class<Doc>> = generateId()
@ -311,3 +275,106 @@ export async function createProjectType (
return tmpl
}
/**
* @public
*/
export async function updateProjectType (
client: TxOperations,
projectType: Ref<ProjectType>,
tasks: TaskTypeWithFactory[]
): Promise<void> {
const current = await client.findOne(task.class.ProjectType, { _id: projectType })
if (current === undefined) {
throw new PlatformError(unknownStatus('No project type found'))
}
const _tasks: Ref<TaskType>[] = [...current.tasks]
const tasksData = new Map<Ref<TaskType>, Data<TaskType>>()
const _statues = new Set<Ref<Status>>()
const hasUpdates = await createTaskTypes(tasks, projectType, client, _statues, tasksData, _tasks, true)
if (hasUpdates) {
const ttypes = await client.findAll<TaskType>(task.class.TaskType, { _id: { $in: _tasks } })
const newStatuses = calculateStatuses(
{
statuses: current.statuses,
tasks: _tasks
},
new Map(ttypes.map((it) => [it._id, it])),
[]
)
await client.update(current, {
tasks: _tasks,
statuses: newStatuses
})
}
}
async function createTaskTypes (
tasks: TaskTypeWithFactory[],
_id: Ref<ProjectType>,
client: TxOperations,
_statues: Set<Ref<Status>>,
tasksData: Map<Ref<TaskType>, Data<TaskType>>,
_tasks: Ref<TaskType>[],
skipExisting: boolean
): Promise<boolean> {
const existingTaskTypes = await client.findAll(task.class.TaskType, { parent: _id })
let hasUpdates = false
for (const it of tasks) {
const { factory, _id: taskId, ...data } = it
if (skipExisting) {
const existingOne = existingTaskTypes.find((tt) => tt.ofClass === data.ofClass)
if (existingOne !== undefined) {
// We have similar one, let's check categories
continue
}
}
hasUpdates = true
const statuses = await createStates(client, 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)
}
return hasUpdates
}

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { IdMap, Ref, Status, WithLookup } from '@hcengineering/core'
import { ProjectType } from '@hcengineering/task'
import { ProjectType, TaskType } from '@hcengineering/task'
import { typeStore } from '@hcengineering/task-resources'
import { IssueStatus } from '@hcengineering/tracker'
import {
@ -36,6 +36,7 @@
export let value: Ref<IssueStatus> | undefined
export let type: Ref<ProjectType> | undefined
export let taskType: Ref<TaskType> | undefined = undefined
let statuses: WithLookup<IssueStatus>[] | undefined = undefined
@ -70,7 +71,14 @@
if (typeId === undefined) return []
const type = types.get(typeId)
if (type === undefined) return []
return type.statuses.map((p) => statuses.get(p._id)).filter((p) => p !== undefined) as IssueStatus[]
let vals = type.statuses
if (taskType !== undefined) {
vals = vals.filter((it) => it.taskType === taskType)
}
return vals
.filter((it, idx, arr) => arr.findIndex((q) => q._id === it._id) === idx)
.map((p) => statuses.get(p._id))
.filter((p) => p !== undefined) as IssueStatus[]
}
function getSelectedStatus (

View File

@ -42,7 +42,7 @@
import tracker from '../../plugin'
import StatusSelector from '../issues/StatusSelector.svelte'
import ChangeIdentity from './ChangeIdentity.svelte'
import { typeStore } from '@hcengineering/task-resources'
import { typeStore, taskTypeStore } from '@hcengineering/task-resources'
export let project: Project | undefined = undefined
@ -70,7 +70,7 @@
const dispatch = createEventDispatcher()
$: isNew = !project
$: isNew = project == null
async function handleSave (): Promise<void> {
if (isNew) {
@ -239,12 +239,13 @@
<div class="antiGrid-row__header">
<Label label={task.string.ProjectType} />
</div>
<Component
is={task.component.ProjectTypeSelector}
disabled={!isNew}
props={{
descriptors: [tracker.descriptors.ProjectType],
type: typeId,
disabled: !isNew,
focusIndex: 4,
kind: 'regular',
size: 'large'
@ -379,7 +380,15 @@
<div class="antiGrid-row__header">
<Label label={tracker.string.DefaultIssueStatus} />
</div>
<StatusSelector bind:value={defaultStatus} type={typeId} kind={'regular'} size={'large'} />
<StatusSelector
taskType={Array.from($taskTypeStore.values()).filter(
(it) => it.parent === typeId && it.ofClass === tracker.class.Issue
)[0]?._id}
bind:value={defaultStatus}
type={typeId}
kind={'regular'}
size={'large'}
/>
</div>
</div>
</Card>

View File

@ -34,7 +34,7 @@ import {
} from '@hcengineering/core'
import { Asset, IntlString, Plugin, Resource, plugin } from '@hcengineering/platform'
import { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
import { ProjectTypeDescriptor, Task, Project as TaskProject, TaskType } from '@hcengineering/task'
import { ProjectTypeDescriptor, Task, Project as TaskProject, TaskType, TaskTypeDescriptor } from '@hcengineering/task'
import { AnyComponent, ComponentExtensionId, Location, ResolvedLocation } from '@hcengineering/ui'
import { Action, ActionCategory, IconProps } from '@hcengineering/view'
@ -455,7 +455,8 @@ export default plugin(trackerId, {
Tracker: '' as Ref<ActionCategory>
},
descriptors: {
ProjectType: '' as Ref<ProjectTypeDescriptor>
ProjectType: '' as Ref<ProjectTypeDescriptor>,
Issue: '' as Ref<TaskTypeDescriptor>
},
action: {
SetDueDate: '' as Ref<Action>,

View File

@ -60,6 +60,7 @@
export let shouldShowAvatar = false
export let autoSelect = false
export let findDefault: (() => Promise<Doc | undefined>) | undefined = undefined
export let groupBy = '_class'
export let create: ObjectCreate | undefined = undefined
@ -116,7 +117,8 @@
placeholder,
create,
searchField,
docProps
docProps,
groupBy
},
!$$slots.content ? container : getEventPositionElement(ev),
(result) => {

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Person } from '@hcengineering/contact'
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { type Class, type Doc, type DocumentQuery, type FindOptions, type Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import presentation, { ObjectCreate, ObjectPopup } from '@hcengineering/presentation'
import ObjectPresenter from './ObjectPresenter.svelte'
@ -34,6 +34,7 @@
export let create: ObjectCreate | undefined = undefined
export let searchField: string = 'name'
export let docProps: Record<string, any> = {}
export let groupBy = '_class'
</script>
<ObjectPopup
@ -46,7 +47,7 @@
{titleDeselect}
{placeholder}
{docQuery}
groupBy={'_class'}
{groupBy}
bind:selectedObjects
bind:ignoreObjects
{shadows}