Fix tracker templates issues (#7590)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2025-01-07 10:54:21 +05:00 committed by GitHub
parent 1254f0154a
commit 54d9a6c847
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 123 additions and 47 deletions

View File

@ -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) {

View File

@ -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}

View File

@ -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,

View File

@ -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}

View File

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

View File

@ -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>

View File

@ -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} />

View File

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