Fix creating suubissues from template (#4610)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-02-13 10:40:53 +06:00 committed by GitHub
parent 7794771694
commit 19da703193
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 330 additions and 25 deletions

View File

@ -66,6 +66,7 @@ import {
type TimeSpendReport
} from '@hcengineering/tracker'
import tracker from './plugin'
import { type TaskType } from '@hcengineering/task'
export const DOMAIN_TRACKER = 'tracker' as Domain
@ -271,6 +272,9 @@ export class TIssueTemplate extends TDoc implements IssueTemplate {
@Prop(ArrOf(TypeRef(tags.class.TagElement)), tracker.string.Labels)
labels?: Ref<TagElement>[]
@Prop(TypeRef(task.class.TaskType), task.string.TaskType)
kind?: Ref<TaskType>
declare space: Ref<Project>
@Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate)

View File

@ -2,7 +2,7 @@
import { Class, Doc, Ref, toIdMap } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import task, { ProjectType, TaskType } from '@hcengineering/task'
import { DropdownLabels } from '@hcengineering/ui'
import { ButtonKind, DropdownLabels } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte'
import { selectedTaskTypeStore, taskTypeStore } from '../..'
@ -10,6 +10,7 @@
export let projectType: Ref<ProjectType> | undefined
export let focusIndex: number = -1
export let baseClass: Ref<Class<Doc>> | undefined = undefined
export let kind: ButtonKind = 'regular'
export let allTypes = false
const client = getClient()
@ -44,12 +45,5 @@
</script>
{#if projectType !== undefined && items.length > 1}
<DropdownLabels
{focusIndex}
kind={'regular'}
{items}
bind:selected={value}
enableSearch={false}
on:selected={change}
/>
<DropdownLabels {focusIndex} {kind} {items} bind:selected={value} enableSearch={false} on:selected={change} />
{/if}

View File

@ -17,7 +17,17 @@
import { AttachmentPresenter, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import chunter from '@hcengineering/chunter'
import { Employee } from '@hcengineering/contact'
import core, { Account, Class, Doc, DocData, Ref, SortingOrder, fillDefaults, generateId } from '@hcengineering/core'
import core, {
Account,
Class,
Doc,
DocData,
Ref,
SortingOrder,
fillDefaults,
generateId,
toIdMap
} from '@hcengineering/core'
import { getResource, translate } from '@hcengineering/platform'
import preference, { SpacePreference } from '@hcengineering/preference'
import {
@ -64,6 +74,7 @@
import { activeComponent, activeMilestone, generateIssueShortLink, updateIssueRelation } from '../issues'
import tracker from '../plugin'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SubIssues from './SubIssues.svelte'
import ComponentSelector from './components/ComponentSelector.svelte'
import AssigneeEditor from './issues/AssigneeEditor.svelte'
import IssueNotification from './issues/IssueNotification.svelte'
@ -107,7 +118,7 @@
let project: Project | undefined
let object = getDefaultObjectFromDraft() ?? getDefaultObject(id)
let isAssigneeTouched = false
let kind: Ref<TaskType> | undefined
let kind: Ref<TaskType> | undefined = undefined
let templateId: Ref<IssueTemplate> | undefined = draft?.template?.template
let appliedTemplateId: Ref<IssueTemplate> | undefined = draft?.template?.template
@ -156,6 +167,7 @@
_id: id ?? generateId(),
title: '',
description: '',
kind: '' as Ref<TaskType>,
priority: priority ?? IssuePriority.NoPriority,
space: _space as Ref<Project>,
component: component ?? $activeComponent ?? null,
@ -258,14 +270,34 @@
}
const { _class, _id, space, children, comments, attachments, labels, description, ...templBase } = template
const allLabels = new Set<Ref<TagElement>>()
for (const label of labels ?? []) {
allLabels.add(label)
}
for (const child of children) {
for (const label of child.labels ?? []) {
allLabels.add(label)
}
}
const tagElements = toIdMap(await client.findAll(tags.class.TagElement, { _id: { $in: Array.from(allLabels) } }))
object.subIssues = template.children.map((p) => {
return {
...p,
_id: p.id,
kind: p.kind ?? kind ?? ('' as Ref<TaskType>),
_id: generateId(),
space: _space as Ref<Project>,
subIssues: [],
dueDate: null,
labels: [],
labels:
p.labels !== undefined
? (p.labels
.map((p) => {
const val = tagElements.get(p)
return val !== undefined ? tagAsRef(val) : undefined
})
.filter((p) => p !== undefined) as TagReference[])
: [],
status: currentProject?.defaultIssueStatus
}
})
@ -279,8 +311,19 @@
}
}
appliedTemplateId = templateId
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: labels } })
object.labels = tagElements.map(tagAsRef)
object.labels =
labels !== undefined
? (labels
.map((p) => {
const val = tagElements.get(p)
return val !== undefined ? tagAsRef(val) : undefined
})
.filter((p) => p !== undefined) as TagReference[])
: []
if (object.kind !== undefined) {
kind = object.kind
}
fillDefaults(hierarchy, object, tracker.class.Issue)
}
@ -336,6 +379,8 @@
return value.trim()
}
let subIssuesComponent: SubIssues
export function canClose (): boolean {
return true
}
@ -439,6 +484,15 @@
await operations.commit()
await descriptionBox.createAttachments(_id)
const parents = parentIssue
? [
{ parentId: _id, parentTitle: value.title, space: parentIssue.space },
{ parentId: parentIssue._id, parentTitle: parentIssue.title, space: parentIssue.space },
...parentIssue.parents
]
: [{ parentId: _id, parentTitle: value.title, space: _space }]
await subIssuesComponent.save(parents, _id)
addNotification(
await translate(tracker.string.IssueCreated, {}, $themeStore.language),
getTitle(object.title),
@ -727,6 +781,16 @@
/>
{/key}
</div>
{#if _space}
<SubIssues
bind:this={subIssuesComponent}
projectId={_space}
project={currentProject}
milestone={object.milestone}
component={object.component}
bind:subIssues={object.subIssues}
/>
{/if}
<DocCreateExtComponent manager={docCreateManager} kind={'body'} space={currentProject} props={extraProps} />
<svelte:fragment slot="pool">
<div id="status-editor">

View File

@ -0,0 +1,199 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { deleteFile } from '@hcengineering/attachment-resources/src/utils'
import core, { AttachedData, Doc, Ref, SortingOrder } from '@hcengineering/core'
import { DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import { calcRank } from '@hcengineering/task'
import { Component, Issue, IssueDraft, IssueParentInfo, Milestone, Project } from '@hcengineering/tracker'
import { Button, ExpandCollapse, Scroller } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../plugin'
import Collapsed from './icons/Collapsed.svelte'
import Expanded from './icons/Expanded.svelte'
import DraftIssueChildList from './templates/DraftIssueChildList.svelte'
export let projectId: Ref<Project>
export let project: Project | undefined
export let milestone: Ref<Milestone> | null = null
export let component: Ref<Component> | null = null
export let subIssues: IssueDraft[] = []
let lastProject = project
let isCollapsed = false
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number, toIndex: number }>) {
if (subIssues) {
const { fromIndex, toIndex } = ev.detail
const [fromIssue] = subIssues.splice(fromIndex, 1)
const leftPart = subIssues.slice(0, toIndex)
const rightPart = subIssues.slice(toIndex)
subIssues = [...leftPart, fromIssue, ...rightPart]
}
}
$: onProjectChange(project)
function onProjectChange (project: Project | undefined) {
if (lastProject?._id === project?._id) return
lastProject = project
if (project === undefined) return
subIssues.forEach((p) => {
p.status = project.defaultIssueStatus
p.space = project._id
})
}
const client = getClient()
// TODO: move to utils
export async function save (parents: IssueParentInfo[], _id: Ref<Doc>) {
if (project === undefined) return
saved = true
for (const subIssue of subIssues) {
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.updateDoc(
tracker.class.Project,
core.space.Space,
project._id,
{
$inc: { sequence: 1 }
},
true
)
const number = (incResult as any).object.sequence
const childId = subIssue._id
const cvalue: AttachedData<Issue> = {
title: subIssue.title.trim(),
description: subIssue.description,
assignee: subIssue.assignee,
component: subIssue.component,
milestone: subIssue.milestone,
number,
status: subIssue.status ?? project.defaultIssueStatus,
priority: subIssue.priority,
rank: calcRank(lastOne, undefined),
comments: 0,
subIssues: 0,
dueDate: null,
parents,
reportedTime: 0,
remainingTime: 0,
estimation: subIssue.estimation,
reports: 0,
relations: [],
childInfo: [],
kind: subIssue.kind,
identifier: `${project.identifier}-${number}`
}
await client.addCollection(
tracker.class.Issue,
project._id,
_id,
tracker.class.Issue,
'subIssues',
cvalue,
childId
)
if ((subIssue.labels?.length ?? 0) > 0) {
for (const label of subIssue.labels) {
await client.addCollection(tags.class.TagReference, project._id, childId, tracker.class.Issue, 'labels', {
title: label.title,
color: label.color,
tag: label.tag
})
}
}
saveAttachments(childId)
}
}
async function saveAttachments (issue: Ref<Issue>) {
const draftAttachments = $draftsStore[`${issue}_attachments`]
if (draftAttachments) {
for (const key in draftAttachments) {
await saveAttachment(draftAttachments[key as Ref<Attachment>], issue)
}
}
removeDraft(issue)
}
async function saveAttachment (doc: Attachment, issue: Ref<Issue>): Promise<void> {
await client.addCollection(
attachment.class.Attachment,
projectId,
issue,
tracker.class.Issue,
'attachments',
doc,
doc._id
)
}
export function load (value: IssueDraft[]) {
subIssues = value
}
let saved = false
onDestroy(() => {
if (!saved) {
subIssues.forEach((st) => {
removeDraft(st._id, true)
})
}
})
// TODO: move to utils
export async function removeDraft (_id: string, removeFiles: boolean = false): Promise<void> {
const draftAttachments = $draftsStore[`${_id}_attachments`]
DraftController.remove(`${_id}_attachments`)
if (removeFiles && draftAttachments) {
for (const key in draftAttachments) {
const attachment = draftAttachments[key as Ref<Attachment>]
await deleteFile(attachment.file)
}
}
}
</script>
<!-- TODO: check if sub issues list is empty in a parent component -->
{#if subIssues.length > 0}
<div class="flex-between clear-mins">
<Button
width="min-content"
icon={isCollapsed ? Collapsed : Expanded}
size="small"
kind="ghost"
label={tracker.string.SubIssuesList}
labelParams={{ subIssues: subIssues.length }}
on:click={() => (isCollapsed = !isCollapsed)}
/>
</div>
<ExpandCollapse isExpanded={!isCollapsed} on:changeContent>
<div class="flex-col flex-no-shrink max-h-30 list clear-mins" class:collapsed={isCollapsed}>
<Scroller>
<DraftIssueChildList
{component}
{milestone}
bind:issues={subIssues}
project={projectId}
on:move={handleIssueSwap}
on:update-issue
/>
</Scroller>
</div>
</ExpandCollapse>
{/if}
<style lang="scss">
.list {
border-top: 1px solid var(--divider-color);
&.collapsed {
padding-top: 1px;
border-top: none;
}
}
</style>

View File

@ -17,13 +17,15 @@
import { Account, Doc, generateId, Ref } from '@hcengineering/core'
import presentation, { DraftController, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { TaskType } from '@hcengineering/task'
import { TaskKindSelector } from '@hcengineering/task-resources'
import {
Component as ComponentType,
Issue,
IssueDraft,
IssuePriority,
Project,
Milestone
Milestone,
Project
} from '@hcengineering/tracker'
import { Button, Component, EditBox } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte'
@ -65,6 +67,7 @@
_id: generateId(),
title: '',
description: '',
kind: '' as Ref<TaskType>,
assignee: project.defaultAssignee ?? null,
status: project.defaultIssueStatus,
space: project._id,
@ -208,6 +211,12 @@
on:change={({ detail }) => (object.priority = detail)}
/>
</div>
<TaskKindSelector
projectType={project.type}
kind="no-border"
bind:value={object.kind}
baseClass={tracker.class.Issue}
/>
<div id="sub-issue-assignee-editor">
{#key object.assignee}
<AssigneeEditor

View File

@ -14,9 +14,10 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { createQuery, ActionContext } from '@hcengineering/presentation'
import tracker, { Component, IssueDraft, Project, Milestone } from '@hcengineering/tracker'
import { eventToHTMLElement, IconCircles, showPopup } from '@hcengineering/ui'
import { ActionContext, createQuery } from '@hcengineering/presentation'
import { TaskKindSelector } from '@hcengineering/task-resources'
import tracker, { Component, IssueDraft, Milestone, Project } from '@hcengineering/tracker'
import { IconCircles, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { FixedColumn } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
@ -137,7 +138,6 @@
value={{ ...issue, space: project }}
kind="list"
size="small"
shouldShowLabel={true}
on:change={({ detail }) => (issue.status = detail)}
/>
<PriorityEditor
@ -173,6 +173,12 @@
</span>
</div>
<div class="flex-center flex-no-shrink">
<TaskKindSelector
projectType={currentProject?.type}
kind={'link'}
bind:value={issue.kind}
baseClass={tracker.class.Issue}
/>
<EstimationEditor
kind={'link'}
size={'large'}

View File

@ -16,11 +16,12 @@
import { generateId, Ref } from '@hcengineering/core'
import presentation, { createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { TaskKindSelector } from '@hcengineering/task-resources'
import { StyledTextArea } from '@hcengineering/text-editor'
import {
Component as ComponentType,
IssuePriority,
IssueTemplateChild,
Component as ComponentType,
Milestone,
Project
} from '@hcengineering/tracker'
@ -108,6 +109,18 @@
labels = [...labels, tag]
}
const projectQuery = createQuery()
$: projectQuery.query(
tracker.class.Project,
{
_id: projectId
},
(res) => {
;[currentProject] = res
}
)
let currentProject: Project | undefined = undefined
$: thisRef && thisRef.scrollIntoView({ behavior: 'smooth' })
$: canSave = getTitle(newIssue.title ?? '').length > 0
@ -138,6 +151,12 @@
</div>
<div class="mt-4 flex-between items-end">
<div class="inline-flex flex-wrap xsmall-gap">
<TaskKindSelector
projectType={currentProject?.type}
kind="no-border"
bind:value={newIssue.kind}
baseClass={tracker.class.Issue}
/>
<PriorityEditor
value={newIssue}
shouldShowLabel

View File

@ -14,8 +14,9 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { createQuery, ActionContext } from '@hcengineering/presentation'
import tracker, { Component, Issue, IssueTemplateChild, Project, Milestone } from '@hcengineering/tracker'
import { ActionContext, createQuery } from '@hcengineering/presentation'
import { TaskKindSelector } from '@hcengineering/task-resources'
import tracker, { Component, Issue, IssueTemplateChild, Milestone, Project } from '@hcengineering/tracker'
import { IconCircles, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { FixedColumn } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
@ -41,6 +42,7 @@
IssueTemplateChildEditor,
{
showBorder: true,
projectId: project,
milestone,
component,
childIssue: target
@ -168,6 +170,12 @@
</span>
</div>
<div class="flex-center flex-no-shrink">
<TaskKindSelector
projectType={currentProject?.type}
kind={'link'}
bind:value={issue.kind}
baseClass={tracker.class.Issue}
/>
<EstimationEditor
kind={'link'}
size={'large'}

View File

@ -210,7 +210,7 @@ export interface Issue extends Task {
* @public
*/
export interface IssueDraft {
kind?: Ref<TaskType>
kind: Ref<TaskType>
_id: Ref<Issue>
title: string
description: Markup
@ -253,6 +253,8 @@ export interface IssueTemplateData {
estimation: number
labels?: Ref<TagElement>[]
kind?: Ref<TaskType>
}
/**