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' })