From 54d9a6c84728c0ac9a1a24ac2aaba6bff343eaac Mon Sep 17 00:00:00 2001
From: Denis Bykhov <bykhov.denis@gmail.com>
Date: Tue, 7 Jan 2025 10:54:21 +0500
Subject: [PATCH] Fix tracker templates issues (#7590)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
---
 .../src/components/AttachmentRefInput.svelte  |  6 ++--
 .../taskTypes/TaskKindSelector.svelte         |  7 +++-
 .../templates/CreateIssueTemplate.svelte      | 35 ++++++++++++++++---
 .../templates/IssueTemplateChildEditor.svelte | 25 ++++++++-----
 .../templates/IssueTemplateChildList.svelte   | 32 +++++++++++------
 .../templates/IssueTemplateChilds.svelte      | 21 ++++++-----
 .../templates/TemplateControlPanel.svelte     | 32 +++++++++++++++--
 .../model/tracker/template-details-page.ts    | 12 +++----
 8 files changed, 123 insertions(+), 47 deletions(-)

diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte
index 1c8758b1ea..74fa2b1234 100644
--- a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte
+++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte
@@ -14,7 +14,7 @@
 -->
 <script lang="ts">
   import { Attachment } from '@hcengineering/attachment'
-  import { RateLimiter, Account, Class, Doc, IdMap, Markup, Ref, Space, generateId, toIdMap } from '@hcengineering/core'
+  import { Account, Class, Doc, IdMap, Markup, RateLimiter, Ref, Space, generateId, toIdMap } from '@hcengineering/core'
   import { Asset, IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
   import {
     DraftController,
@@ -25,9 +25,9 @@
     getFileMetadata,
     uploadFile
   } from '@hcengineering/presentation'
+  import { EmptyMarkup } from '@hcengineering/text'
   import textEditor, { type RefAction } from '@hcengineering/text-editor'
   import { AttachIcon, ReferenceInput } from '@hcengineering/text-editor-resources'
-  import { EmptyMarkup } from '@hcengineering/text'
   import { Loading, type AnySvelteComponent } from '@hcengineering/ui'
   import { createEventDispatcher, onDestroy, tick } from 'svelte'
   import attachment from '../plugin'
@@ -196,11 +196,11 @@
   }
 
   async function fileDrop (e: DragEvent): Promise<void> {
-    progress = true
     const list = e.dataTransfer?.files
     const limiter = new RateLimiter(10)
 
     if (list === undefined || list.length === 0) return
+    progress = true
     for (let index = 0; index < list.length; index++) {
       const file = list.item(index)
       if (file !== null) {
diff --git a/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte b/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte
index c37fa94106..a1f18726fb 100644
--- a/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte
+++ b/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte
@@ -12,6 +12,9 @@
   export let baseClass: Ref<Class<Doc>> | undefined = undefined
   export let kind: ButtonKind = 'regular'
   export let size: ButtonSize = 'medium'
+  export let justify: 'left' | 'center' = 'center'
+  export let width: string | undefined = undefined
+  export let showAlways: boolean = false
   export let allTypes = false
 
   const client = getClient()
@@ -46,12 +49,14 @@
   }
 </script>
 
-{#if projectType !== undefined && items.length > 1}
+{#if projectType !== undefined && (items.length > 1 || showAlways)}
   <DropdownLabels
     {focusIndex}
     {kind}
     {size}
     {items}
+    {justify}
+    {width}
     dataId={'btnSelectTaskType'}
     bind:selected={value}
     enableSearch={false}
diff --git a/plugins/tracker-resources/src/components/templates/CreateIssueTemplate.svelte b/plugins/tracker-resources/src/components/templates/CreateIssueTemplate.svelte
index 7a403dc35b..514709da90 100644
--- a/plugins/tracker-resources/src/components/templates/CreateIssueTemplate.svelte
+++ b/plugins/tracker-resources/src/components/templates/CreateIssueTemplate.svelte
@@ -15,8 +15,10 @@
 <script lang="ts">
   import { Person } from '@hcengineering/contact'
   import { Data, Doc, Ref, generateId } from '@hcengineering/core'
-  import { Card, KeyedAttribute, SpaceSelector, getClient } from '@hcengineering/presentation'
-  import tags, { TagElement } from '@hcengineering/tags'
+  import { Card, KeyedAttribute, SpaceSelector, createQuery, getClient } from '@hcengineering/presentation'
+  import tags, { TagElement, TagReference } from '@hcengineering/tags'
+  import { TaskType } from '@hcengineering/task'
+  import { TaskKindSelector } from '@hcengineering/task-resources'
   import { StyledTextBox } from '@hcengineering/text-editor-resources'
   import { Component as ComponentType, IssuePriority, IssueTemplate, Milestone, Project } from '@hcengineering/tracker'
   import { Component, EditBox, Label } from '@hcengineering/ui'
@@ -39,6 +41,7 @@
   export let relatedTo: Doc | undefined
 
   let labels: TagElement[] = []
+  let kind: Ref<TaskType> | undefined = undefined
 
   let objectId: Ref<IssueTemplate> = generateId()
   let object: Data<IssueTemplate> = {
@@ -93,6 +96,7 @@
       comments: 0,
       attachments: 0,
       labels: labels.map((it) => it._id),
+      kind,
       relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : []
     }
 
@@ -119,6 +123,19 @@
   function addTagRef (tag: TagElement): void {
     labels = [...labels, tag]
   }
+
+  let currentProject: Project | undefined
+  const spaceQuery = createQuery()
+  $: if (_space !== undefined) {
+    spaceQuery.query(tracker.class.Project, { _id: _space }, (res) => {
+      currentProject = res[0]
+    })
+  } else {
+    spaceQuery.unsubscribe()
+    currentProject = undefined
+  }
+
+  $: labelRefs = labels.map((it) => ({ ...(it as unknown as TagReference), _id: generateId(), tag: it._id }))
 </script>
 
 <Card
@@ -144,7 +161,17 @@
     />
   </svelte:fragment>
   <svelte:fragment slot="title" let:label>
-    <Label {label} />
+    <div class="flex-row-center gap-2 pt-1 pb-1 pr-1">
+      <span class="overflow-label">
+        <Label {label} />
+      </span>
+      <TaskKindSelector
+        projectType={currentProject?.type}
+        bind:value={kind}
+        baseClass={tracker.class.Issue}
+        size={'small'}
+      />
+    </div>
   </svelte:fragment>
 
   <EditBox
@@ -187,7 +214,7 @@
     <Component
       is={tags.component.TagsDropdownEditor}
       props={{
-        items: labels,
+        items: labelRefs,
         key,
         targetClass: tracker.class.Issue,
         countLabel: tracker.string.NumberLabels,
diff --git a/plugins/tracker-resources/src/components/templates/IssueTemplateChildEditor.svelte b/plugins/tracker-resources/src/components/templates/IssueTemplateChildEditor.svelte
index c6302d010d..7f8ca871fe 100644
--- a/plugins/tracker-resources/src/components/templates/IssueTemplateChildEditor.svelte
+++ b/plugins/tracker-resources/src/components/templates/IssueTemplateChildEditor.svelte
@@ -44,9 +44,11 @@
   const client = getClient()
 
   let newIssue: IssueTemplateChild = childIssue !== undefined ? { ...childIssue } : getIssueDefaults()
-  let thisRef: HTMLDivElement
+  let thisRef: HTMLDivElement | undefined
   let focusIssueTitle: () => void
   let labels: TagElement[] = []
+  let canSave = getTitle(newIssue.title ?? '').length > 0
+  $: canSave = getTitle(newIssue.title ?? '').length > 0
 
   const labelsQuery = createQuery()
 
@@ -72,20 +74,25 @@
     }
   }
 
-  function resetToDefaults () {
+  function resetToDefaults (): void {
     newIssue = getIssueDefaults()
+    labels = []
     focusIssueTitle?.()
   }
 
-  function getTitle (value: string) {
+  function getTitle (value: string): string {
     return value.trim()
   }
 
-  function close () {
+  function close (): void {
     dispatch('close')
   }
 
-  async function createIssue () {
+  function onDelete (): void {
+    dispatch('close', ['delete', newIssue])
+  }
+
+  function createIssue (): void {
     if (!canSave) {
       return
     }
@@ -99,7 +106,7 @@
     if (childIssue === undefined) {
       dispatch('create', value)
     } else {
-      dispatch('close', value)
+      dispatch('close', ['update', value])
     }
 
     resetToDefaults()
@@ -121,8 +128,7 @@
   )
   let currentProject: Project | undefined = undefined
 
-  $: thisRef && thisRef.scrollIntoView({ behavior: 'smooth' })
-  $: canSave = getTitle(newIssue.title ?? '').length > 0
+  $: thisRef !== undefined && thisRef.scrollIntoView({ behavior: 'smooth' })
 
   $: labelRefs = labels.map((it) => ({ ...(it as unknown as TagReference), _id: generateId(), tag: it._id }))
 </script>
@@ -200,6 +206,9 @@
       />
     </div>
     <div class="ml-2 buttons-group small-gap">
+      {#if childIssue !== undefined}
+        <Button label={presentation.string.Delete} size="small" kind="dangerous" on:click={onDelete} />
+      {/if}
       <Button label={presentation.string.Cancel} size="small" kind="ghost" on:click={close} />
       <Button
         disabled={!canSave}
diff --git a/plugins/tracker-resources/src/components/templates/IssueTemplateChildList.svelte b/plugins/tracker-resources/src/components/templates/IssueTemplateChildList.svelte
index 1bb28635a3..e64781aa5b 100644
--- a/plugins/tracker-resources/src/components/templates/IssueTemplateChildList.svelte
+++ b/plugins/tracker-resources/src/components/templates/IssueTemplateChildList.svelte
@@ -37,7 +37,7 @@
   let dragIndex: number | null = null
   let hoveringIndex: number | null = null
 
-  function openIssue (evt: MouseEvent, target: IssueTemplateChild) {
+  function openIssue (evt: MouseEvent, target: IssueTemplateChild): void {
     showPopup(
       IssueTemplateChildEditor,
       {
@@ -48,26 +48,32 @@
         childIssue: target
       },
       eventToHTMLElement(evt),
-      (evt: IssueTemplateChild | undefined | null) => {
+      (evt: ['update' | 'delete', IssueTemplateChild] | undefined | null) => {
         if (evt != null) {
-          const pos = issues.findIndex((it) => it.id === evt.id)
+          const pos = issues.findIndex((it) => it.id === target.id)
           if (pos !== -1) {
-            issues[pos] = evt
-            dispatch('update-issue', evt)
+            if (evt[0] === 'delete') {
+              issues.splice(pos, 1)
+              issues = issues
+              dispatch('update-issues', issues)
+            } else {
+              issues[pos] = evt[1]
+              dispatch('update-issue', evt[1])
+            }
           }
         }
       }
     )
   }
 
-  function resetDrag () {
+  function resetDrag (): void {
     dragId = null
     dragIndex = null
     hoveringIndex = null
   }
 
-  function handleDragStart (ev: DragEvent, index: number, item: IssueTemplateChild) {
-    if (ev.dataTransfer) {
+  function handleDragStart (ev: DragEvent, index: number, item: IssueTemplateChild): void {
+    if (ev.dataTransfer != null) {
       ev.dataTransfer.effectAllowed = 'move'
       ev.dataTransfer.dropEffect = 'move'
       dragIndex = index
@@ -75,8 +81,8 @@
     }
   }
 
-  function handleDrop (ev: DragEvent, toIndex: number) {
-    if (ev.dataTransfer && dragIndex !== null && toIndex !== dragIndex) {
+  function handleDrop (ev: DragEvent, toIndex: number): void {
+    if (ev.dataTransfer != null && dragIndex !== null && toIndex !== dragIndex) {
       ev.dataTransfer.dropEffect = 'move'
 
       dispatch('move', { id: dragId, toIndex })
@@ -98,7 +104,7 @@
   let currentProject: Project | undefined = undefined
 
   function getIssueTemplateId (currentProject: Project | undefined, issue: IssueTemplateChild): string {
-    return currentProject
+    return currentProject !== undefined
       ? `${currentProject.identifier}-${issues.findIndex((it) => it.id === issue.id)}`
       : `${issues.findIndex((it) => it.id === issue.id)}}`
   }
@@ -175,6 +181,10 @@
         kind={'link'}
         bind:value={issue.kind}
         baseClass={tracker.class.Issue}
+        on:change={(evt) => {
+          dispatch('update-issue', { id: issue.id, kind: evt.detail })
+          issue.kind = evt.detail
+        }}
       />
       <EstimationEditor
         kind={'link'}
diff --git a/plugins/tracker-resources/src/components/templates/IssueTemplateChilds.svelte b/plugins/tracker-resources/src/components/templates/IssueTemplateChilds.svelte
index 18e404fca9..36ad96da1c 100644
--- a/plugins/tracker-resources/src/components/templates/IssueTemplateChilds.svelte
+++ b/plugins/tracker-resources/src/components/templates/IssueTemplateChilds.svelte
@@ -35,17 +35,15 @@
   let isCollapsed = false
   let isCreating = false
 
-  function handleIssueSwap (ev: CustomEvent<{ id: Ref<Issue>, toIndex: number }>) {
-    if (children) {
-      const { id, toIndex } = ev.detail
-      const index = children.findIndex((p) => p.id === id)
-      if (index !== -1 && index !== toIndex) {
-        const [fromIssue] = children.splice(index, 1)
-        const leftPart = children.slice(0, toIndex)
-        const rightPart = children.slice(toIndex)
-        children = [...leftPart, fromIssue, ...rightPart]
-        dispatch('update-issues', children)
-      }
+  function handleIssueSwap (ev: CustomEvent<{ id: Ref<Issue>, toIndex: number }>): void {
+    const { id, toIndex } = ev.detail
+    const index = children.findIndex((p) => p.id === id)
+    if (index !== -1 && index !== toIndex) {
+      const [fromIssue] = children.splice(index, 1)
+      const leftPart = children.slice(0, toIndex)
+      const rightPart = children.slice(toIndex)
+      children = [...leftPart, fromIssue, ...rightPart]
+      dispatch('update-issues', children)
     }
   }
 
@@ -96,6 +94,7 @@
           {project}
           on:move={handleIssueSwap}
           on:update-issue
+          on:update-issues
         />
       </Scroller>
     </div>
diff --git a/plugins/tracker-resources/src/components/templates/TemplateControlPanel.svelte b/plugins/tracker-resources/src/components/templates/TemplateControlPanel.svelte
index beaa5843be..0499c1b9f6 100644
--- a/plugins/tracker-resources/src/components/templates/TemplateControlPanel.svelte
+++ b/plugins/tracker-resources/src/components/templates/TemplateControlPanel.svelte
@@ -14,8 +14,10 @@
 -->
 <script lang="ts">
   import { generateId, Ref, WithLookup } from '@hcengineering/core'
-  import { AttributeBarEditor, getClient, KeyedAttribute } from '@hcengineering/presentation'
+  import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation'
   import tags, { TagElement, TagReference } from '@hcengineering/tags'
+  import task, { Project } from '@hcengineering/task'
+  import { TaskKindSelector } from '@hcengineering/task-resources'
   import type { IssueTemplate } from '@hcengineering/tracker'
   import { Component, Label } from '@hcengineering/ui'
   import { getFiltredKeys, isCollectionAttr } from '@hcengineering/view-resources'
@@ -37,7 +39,7 @@
     keys = filtredKeys.filter((key) => !isCollectionAttr(hierarchy, key))
   }
 
-  $: updateKeys(['title', 'description', 'priority', 'number', 'assignee', 'component', 'milestone'])
+  $: updateKeys(['title', 'description', 'priority', 'number', 'assignee', 'component', 'milestone', 'kind'])
 
   const key: KeyedAttribute = {
     key: 'labels',
@@ -62,13 +64,37 @@
       })
     }
   }
+
+  let currentProject: Project | undefined
+  const spaceQuery = createQuery()
+  spaceQuery.query(tracker.class.Project, { _id: issue.space }, (res) => {
+    currentProject = res[0]
+  })
 </script>
 
 <div class="popupPanel-body__aside-grid">
+  <span class="labelOnPanel">
+    <Label label={task.string.TaskType} />
+  </span>
+  <TaskKindSelector
+    projectType={currentProject?.type}
+    value={issue.kind}
+    baseClass={tracker.class.Issue}
+    justify={'left'}
+    width={'100%'}
+    size={'medium'}
+    kind={'link'}
+    showAlways
+    on:change={async (evt) => {
+      if (evt.detail !== undefined) {
+        await client.update(issue, { kind: evt.detail })
+      }
+    }}
+  />
   <span class="labelOnPanel">
     <Label label={tracker.string.Priority} />
   </span>
-  <PriorityEditor value={issue} size={'medium'} shouldShowLabel />
+  <PriorityEditor value={issue} size={'medium'} justify={'left'} width={'100%'} shouldShowLabel />
 
   <span class="labelOnPanel">
     <Label label={tracker.string.Assignee} />
diff --git a/tests/sanity/tests/model/tracker/template-details-page.ts b/tests/sanity/tests/model/tracker/template-details-page.ts
index eb44b9bc75..478aa35278 100644
--- a/tests/sanity/tests/model/tracker/template-details-page.ts
+++ b/tests/sanity/tests/model/tracker/template-details-page.ts
@@ -6,13 +6,13 @@ import { convertEstimation } from '../../tracker/tracker.utils'
 export class TemplateDetailsPage extends CommonTrackerPage {
   inputTitle = (): Locator => this.page.locator('div.popupPanel-body input[type="text"]')
   inputDescription = (): Locator => this.page.locator('div.popupPanel-body div.textInput p')
-  buttonPriority = (): Locator => this.page.locator('//span[text()="Priority"]/../button[1]//span')
-  buttonAssignee = (): Locator => this.page.locator('(//span[text()="Assignee"]/../div/button)[1]')
+  buttonPriority = (): Locator => this.page.locator('//span[text()="Priority"]/following-sibling::button[1]//span')
+  buttonAssignee = (): Locator => this.page.locator('//span[text()="Assignee"]/following-sibling::div[1]/button/span')
   textLabels = (dataLabels: string): Locator => this.page.locator('div.menu-group span', { hasText: dataLabels })
-  buttonAddLabel = (): Locator => this.page.locator('//span[text()="Labels"]/../button[2]//span')
-  buttonComponent = (): Locator => this.page.locator('//span[text()="Component"]/../div/div/button')
-  buttonEstimation = (): Locator => this.page.locator('(//span[text()="Estimation"]/../div/button)[3]')
-  buttonDueDate = (): Locator => this.page.locator('(//span[text()="Due date"]/../div/button)[2]')
+  buttonAddLabel = (): Locator => this.page.locator('//span[text()="Labels"]/following-sibling::button[1]//span')
+  buttonComponent = (): Locator => this.page.locator('//span[text()="Component"]/following-sibling::div[1]/div/button')
+  buttonEstimation = (): Locator => this.page.locator('//span[text()="Estimation"]/following-sibling::div[1]/button')
+  buttonDueDate = (): Locator => this.page.locator('//span[text()="Due date"]/following-sibling::div[1]/button')
   buttonSaveDueDate = (): Locator => this.page.locator('div.footer > button')
   activityContent = (): Locator => this.page.locator('div.grid div.content')
   buttonDelete = (): Locator => this.page.locator('button[class*="menuItem"] > span', { hasText: 'Delete' })