Subissues with attachments (#2641)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-16 10:58:52 +06:00 committed by GitHub
parent 54e62d9547
commit 786e51c6e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 805 additions and 134 deletions

View File

@ -40,12 +40,12 @@
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import {
calcRank,
DraftIssueChild,
Issue,
IssueDraft,
IssuePriority,
IssueStatus,
IssueTemplate,
IssueTemplateChild,
Project,
Sprint,
Team
@ -80,7 +80,7 @@
import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SprintSelector from './sprints/SprintSelector.svelte'
import IssueTemplateChilds from './templates/IssueTemplateChilds.svelte'
import SubIssues from './SubIssues.svelte'
export let space: Ref<Team>
export let status: Ref<IssueStatus> | undefined = undefined
@ -96,6 +96,8 @@
const draft: IssueDraft | undefined = shouldSaveDraft ? getUserDraft(tracker.class.IssueDraft) : undefined
let subIssuesComponent: SubIssues
let issueStatuses: WithLookup<IssueStatus>[] | undefined
let labels: TagReference[] = draft?.labels || []
let objectId: Ref<Issue> = draft?.issueId || generateId()
@ -147,9 +149,8 @@
templateId = undefined
template = undefined
object = { ...defaultIssue }
subIssues = []
if (!originalIssue && !draft) {
updateIssueStatusId(_space, status)
updateIssueStatusId(currentTeam, status)
}
}
@ -158,7 +159,7 @@
let template: IssueTemplate | undefined = undefined
const templateQuery = createQuery()
let subIssues: IssueTemplateChild[] = draft?.subIssues || []
let subIssues: DraftIssueChild[] = draft?.subIssues || []
$: if (templateId !== undefined) {
templateQuery.query(tracker.class.IssueTemplate, { _id: templateId }, (res) => {
@ -192,7 +193,9 @@
const { _class, _id, space, children, comments, attachments, labels: labels_, ...templBase } = template
subIssues = template.children
subIssues = template.children.map((p) => {
return { ...p, status: currentTeam?.defaultIssueStatus ?? ('' as Ref<IssueStatus>) }
})
object = {
...object,
@ -226,7 +229,7 @@
}
$: _space = draft?.team || space
$: !originalIssue && !draft && updateIssueStatusId(_space, status)
$: !originalIssue && !draft && updateIssueStatusId(currentTeam, status)
$: canSave = getTitle(object.title ?? '').length > 0
$: statusesQuery.query(
@ -300,16 +303,14 @@
}
}
async function updateIssueStatusId (teamId: Ref<Team>, issueStatusId?: Ref<IssueStatus>) {
async function updateIssueStatusId (currentTeam: Team | undefined, issueStatusId?: Ref<IssueStatus>) {
if (issueStatusId !== undefined) {
object.status = issueStatusId
return
}
const team = await client.findOne(tracker.class.Team, { _id: teamId })
if (team?.defaultIssueStatus) {
object.status = team.defaultIssueStatus
if (currentTeam?.defaultIssueStatus) {
object.status = currentTeam.defaultIssueStatus
}
}
@ -342,9 +343,9 @@
}
}
// if (object.attachments && object.attachments > 0) {
// return false
// }
if (object.attachments && object.attachments > 0) {
return false
}
if (draft.project && draft.project !== defaultIssue.project) {
return false
@ -358,10 +359,8 @@
return true
}
const team = await client.findOne(tracker.class.Team, { _id: _space })
if (team?.defaultIssueStatus) {
return draft.status === team.defaultIssueStatus
if (currentTeam?.defaultIssueStatus) {
return draft.status === currentTeam.defaultIssueStatus
}
return false
@ -470,66 +469,14 @@
}
}
}
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.Team,
core.space.Space,
_space,
{
$inc: { sequence: 1 }
},
true
)
const childId: Ref<Issue> = generateId()
const cvalue: AttachedData<Issue> = {
title: getTitle(subIssue.title),
description: subIssue.description,
assignee: subIssue.assignee,
project: subIssue.project,
sprint: subIssue.sprint,
number: (incResult as any).object.sequence,
status: object.status,
priority: subIssue.priority,
rank: calcRank(lastOne, undefined),
comments: 0,
subIssues: 0,
dueDate: null,
parents: parentIssue
? [
{ parentId: objectId, parentTitle: value.title },
{ parentId: parentIssue._id, parentTitle: parentIssue.title },
...parentIssue.parents
]
: [{ parentId: objectId, parentTitle: value.title }],
reportedTime: 0,
estimation: subIssue.estimation,
reports: 0,
relations: [],
childInfo: []
}
await client.addCollection(
tracker.class.Issue,
_space,
objectId,
tracker.class.Issue,
'subIssues',
cvalue,
childId
)
if ((subIssue.labels?.length ?? 0) > 0) {
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: subIssue.labels } })
for (const label of tagElements) {
await client.addCollection(tags.class.TagReference, _space, childId, tracker.class.Issue, 'labels', {
title: label.title,
color: label.color,
tag: label._id
})
}
}
}
const parents = parentIssue
? [
{ parentId: objectId, parentTitle: value.title },
{ parentId: parentIssue._id, parentTitle: parentIssue.title },
...parentIssue.parents
]
: [{ parentId: objectId, parentTitle: value.title }]
await subIssuesComponent.save(parents)
addNotification(await translate(tracker.string.IssueCreated, {}), getTitle(object.title), IssueNotification, {
issueId: objectId,
subTitlePostfix: (await translate(tracker.string.Created, { value: 1 })).toLowerCase(),
@ -677,14 +624,6 @@
<div class="flex-row-center">
<SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space={_space} />
</div>
<!-- <Button
icon={tracker.icon.Home}
label={presentation.string.Save}
size={'small'}
kind={'no-border'}
disabled
on:click={() => {}}
/> -->
<ObjectBox
_class={tracker.class.IssueTemplate}
value={templateId}
@ -741,13 +680,17 @@
}}
/>
{/key}
<IssueTemplateChilds
bind:children={subIssues}
sprint={object.sprint}
project={object.project}
isScrollable
maxHeight="limited"
/>
{#if issueStatuses}
<SubIssues
bind:this={subIssuesComponent}
teamId={_space}
parent={objectId}
statuses={issueStatuses ?? []}
team={currentTeam}
sprint={object.sprint}
project={object.project}
/>
{/if}
<svelte:fragment slot="pool">
{#if issueStatuses}
<div id="status-editor">

View File

@ -0,0 +1,252 @@
<!--
// 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, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { draftStore, getClient, updateDraftStore } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import {
calcRank,
DraftIssueChild,
Issue,
IssueParentInfo,
IssueStatus,
Project,
Sprint,
Team
} from '@hcengineering/tracker'
import { Button, closeTooltip, ExpandCollapse, IconAdd, 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 DraftIssueChildEditor from './templates/DraftIssueChildEditor.svelte'
import DraftIssueChildList from './templates/DraftIssueChildList.svelte'
export let parent: Ref<Issue>
export let teamId: Ref<Team>
export let team: Team | undefined
export let sprint: Ref<Sprint> | null = null
export let project: Ref<Project> | null = null
export let subIssues: DraftIssueChild[] = []
export let statuses: WithLookup<IssueStatus>[]
let isCollapsed = false
let isCreating = 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]
}
}
const client = getClient()
export async function save (parents: IssueParentInfo[]) {
if (team === 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.Team,
core.space.Space,
team._id,
{
$inc: { sequence: 1 }
},
true
)
const childId: Ref<Issue> = subIssue.id as Ref<Issue>
const cvalue: AttachedData<Issue> = {
title: subIssue.title.trim(),
description: subIssue.description,
assignee: subIssue.assignee,
project: subIssue.project,
sprint: subIssue.sprint,
number: (incResult as any).object.sequence,
status: subIssue.status,
priority: subIssue.priority,
rank: calcRank(lastOne, undefined),
comments: 0,
subIssues: 0,
dueDate: null,
parents,
reportedTime: 0,
estimation: subIssue.estimation,
reports: 0,
relations: [],
childInfo: []
}
await client.addCollection(
tracker.class.Issue,
team._id,
parent,
tracker.class.Issue,
'subIssues',
cvalue,
childId
)
if ((subIssue.labels?.length ?? 0) > 0) {
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: subIssue.labels } })
for (const label of tagElements) {
await client.addCollection(tags.class.TagReference, team._id, childId, tracker.class.Issue, 'labels', {
title: label.title,
color: label.color,
tag: label._id
})
}
}
saveAttachments(childId)
}
}
async function saveAttachments (issue: Ref<Issue>) {
const draftAttachments: Record<Ref<Attachment>, Attachment> | undefined = $draftStore[issue]
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,
teamId,
issue,
tracker.class.Issue,
'attachments',
doc,
doc._id
)
}
export function load (value: DraftIssueChild[]) {
subIssues = value
}
let saved = false
onDestroy(() => {
if (!saved) {
subIssues.forEach((st) => {
removeDraft(st.id, true)
})
}
})
export async function removeDraft (_id: string, removeFiles: boolean = false): Promise<void> {
const draftAttachments: Record<Ref<Attachment>, Attachment> | undefined = $draftStore[_id]
updateDraftStore(_id, undefined)
if (removeFiles && draftAttachments) {
for (const key in draftAttachments) {
const attachment = draftAttachments[key as Ref<Attachment>]
await deleteFile(attachment.file)
}
}
}
$: hasSubIssues = subIssues.length > 0
</script>
<div class="flex-between clear-mins">
{#if hasSubIssues}
<Button
width="min-content"
icon={isCollapsed ? Collapsed : Expanded}
size="small"
kind="transparent"
label={tracker.string.SubIssuesList}
labelParams={{ subIssues: subIssues.length }}
on:click={() => {
isCollapsed = !isCollapsed
isCreating = false
}}
/>
{/if}
<Button
id="add-sub-issue"
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 } }}
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
</div>
{#if hasSubIssues}
<ExpandCollapse isExpanded={!isCollapsed} duration={400} on:changeContent>
<div class="flex-col flex-no-shrink max-h-30 list clear-mins" class:collapsed={isCollapsed}>
<Scroller>
<DraftIssueChildList
{statuses}
{project}
{sprint}
bind:issues={subIssues}
team={teamId}
on:move={handleIssueSwap}
on:update-issue
/>
</Scroller>
</div>
</ExpandCollapse>
{/if}
{#if isCreating && team}
<ExpandCollapse isExpanded={!isCollapsed} duration={400} on:changeContent>
<DraftIssueChildEditor
{team}
{statuses}
{project}
{sprint}
on:close={() => {
isCreating = false
}}
on:create={(evt) => {
if (subIssues === undefined) {
subIssues = []
}
subIssues = [...subIssues, evt.detail]
}}
on:changeContent
/>
</ExpandCollapse>
{/if}
<style lang="scss">
.list {
border-top: 1px solid var(--divider-color);
&.collapsed {
padding-top: 1px;
border-top: none;
}
}
</style>

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { DraftIssueChild, Issue, IssueStatus, Team } from '@hcengineering/tracker'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
@ -23,7 +23,7 @@
import IssueStatusIcon from './IssueStatusIcon.svelte'
import StatusPresenter from './StatusPresenter.svelte'
export let value: Issue | AttachedData<Issue>
export let value: Issue | AttachedData<Issue> | DraftIssueChild
export let statuses: WithLookup<IssueStatus>[] | undefined = undefined
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false

View File

@ -13,13 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { Employee } from '@hcengineering/contact'
import { Data, Doc, generateId, Ref } from '@hcengineering/core'
import { Card, getClient, KeyedAttribute, SpaceSelector } from '@hcengineering/presentation'
import tags, { TagElement } from '@hcengineering/tags'
import { StyledTextBox } from '@hcengineering/text-editor'
import { IssuePriority, IssueTemplate, Project, Sprint, Team } from '@hcengineering/tracker'
import { Button, Component, EditBox, IconAttachment, Label } from '@hcengineering/ui'
import { Component, EditBox, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { activeProject, activeSprint } from '../../issues'
import tracker from '../../plugin'
@ -58,7 +58,7 @@
const dispatch = createEventDispatcher()
const client = getClient()
let descriptionBox: AttachmentStyledBox
let descriptionBox: StyledTextBox
const key: KeyedAttribute = {
key: 'labels',
@ -150,11 +150,8 @@
</svelte:fragment>
<EditBox bind:value={object.title} placeholder={tracker.string.IssueTitlePlaceholder} kind={'large-style'} focus />
<AttachmentStyledBox
<StyledTextBox
bind:this={descriptionBox}
{objectId}
_class={tracker.class.Issue}
space={_space}
alwaysEdit
showButtons={false}
emphasized
@ -165,7 +162,7 @@
bind:children={object.children}
project={object.project}
sprint={object.sprint}
teamId={spaceRef?.identifier ?? 'TSK'}
team={_space}
maxHeight="limited"
/>
<svelte:fragment slot="pool">
@ -204,13 +201,4 @@
<ProjectSelector value={object.project} onChange={handleProjectIdChanged} />
<SprintSelector value={object.sprint} onChange={handleSprintIdChanged} useProject={object.project ?? undefined} />
</svelte:fragment>
<svelte:fragment slot="footer">
<Button
icon={IconAttachment}
kind={'transparent'}
on:click={() => {
descriptionBox.attach()
}}
/>
</svelte:fragment>
</Card>

View File

@ -0,0 +1,208 @@
<!--
// 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 { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { generateId, Ref, WithLookup } from '@hcengineering/core'
import presentation, { createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import {
DraftIssueChild,
IssuePriority,
IssueStatus,
IssueTemplateChild,
Project,
Sprint,
Team
} from '@hcengineering/tracker'
import { Button, Component, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
import PriorityEditor from '../issues/PriorityEditor.svelte'
import StatusEditor from '../issues/StatusEditor.svelte'
import EstimationEditor from './EstimationEditor.svelte'
export let team: Team
export let sprint: Ref<Sprint> | null = null
export let project: Ref<Project> | null = null
export let childIssue: DraftIssueChild | undefined = undefined
export let showBorder = false
export let statuses: WithLookup<IssueStatus>[]
const dispatch = createEventDispatcher()
const client = getClient()
let newIssue: DraftIssueChild = childIssue !== undefined ? { ...childIssue } : getIssueDefaults()
let thisRef: HTMLDivElement
let focusIssueTitle: () => void
let labels: TagElement[] = []
const labelsQuery = createQuery()
$: labelsQuery.query(tags.class.TagElement, { _id: { $in: childIssue?.labels ?? [] } }, (res) => {
labels = res
})
const key: KeyedAttribute = {
key: 'labels',
attr: client.getHierarchy().getAttribute(tracker.class.IssueTemplate, 'labels')
}
function getIssueDefaults (): DraftIssueChild {
return {
id: generateId(),
title: '',
description: '',
assignee: null,
status: team.defaultIssueStatus,
project,
priority: IssuePriority.NoPriority,
sprint,
estimation: 0
}
}
function resetToDefaults () {
newIssue = getIssueDefaults()
focusIssueTitle?.()
}
function getTitle (value: string) {
return value.trim()
}
function close () {
dispatch('close')
}
async function createIssue () {
if (!canSave) {
return
}
const value: IssueTemplateChild = {
...newIssue,
title: getTitle(newIssue.title),
project: project ?? null,
labels: labels.map((it) => it._id)
}
if (childIssue === undefined) {
dispatch('create', value)
} else {
dispatch('close', value)
}
resetToDefaults()
}
function addTagRef (tag: TagElement): void {
labels = [...labels, tag]
}
$: thisRef && thisRef.scrollIntoView({ behavior: 'smooth' })
$: canSave = getTitle(newIssue.title ?? '').length > 0
$: labelRefs = labels.map((it) => ({ ...(it as unknown as TagReference), _id: generateId(), tag: it._id }))
</script>
<div bind:this={thisRef} class="flex-col antiEmphasized clear-mins" class:antiPopup={showBorder}>
<div class="flex-col w-full clear-mins">
<EditBox
bind:value={newIssue.title}
bind:focusInput={focusIssueTitle}
kind={'large-style'}
placeholder={tracker.string.SubIssueTitlePlaceholder}
focus
/>
<div class="mt-4 clear-mins">
{#key newIssue.id}
<AttachmentStyledBox
objectId={newIssue.id}
space={team._id}
_class={tracker.class.Issue}
bind:content={newIssue.description}
placeholder={tracker.string.SubIssueDescriptionPlaceholder}
showButtons={false}
alwaysEdit
shouldSaveDraft
maxHeight={'limited'}
on:changeContent
/>
{/key}
</div>
</div>
<div class="mt-4 flex-between">
<div class="buttons-group xsmall-gap">
<StatusEditor
value={newIssue}
{statuses}
kind="no-border"
size="small"
shouldShowLabel={true}
on:change={({ detail }) => (newIssue.status = detail)}
/>
<PriorityEditor
value={newIssue}
shouldShowLabel
isEditable
kind="no-border"
size="small"
justify="center"
on:change={({ detail }) => (newIssue.priority = detail)}
/>
{#key newIssue.assignee}
<AssigneeEditor
value={newIssue}
size="small"
kind="no-border"
on:change={({ detail }) => (newIssue.assignee = detail)}
/>
{/key}
<EstimationEditor
kind={'no-border'}
size={'small'}
bind:value={newIssue}
on:change={(evt) => {
newIssue.estimation = evt.detail
}}
/>
<Component
is={tags.component.TagsDropdownEditor}
props={{
items: labelRefs,
key,
targetClass: tracker.class.Issue,
countLabel: tracker.string.NumberLabels
}}
on:open={(evt) => {
addTagRef(evt.detail)
}}
on:delete={(evt) => {
labels = labels.filter((it) => it._id !== evt.detail)
}}
/>
</div>
<div class="buttons-group small-gap">
<Button label={presentation.string.Cancel} size="small" kind="transparent" on:click={close} />
<Button
disabled={!canSave}
label={presentation.string.Save}
size="small"
kind="no-border"
on:click={createIssue}
/>
</div>
</div>
</div>

View File

@ -0,0 +1,264 @@
<!--
// 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 { Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import tracker, {
DraftIssueChild,
IssueStatus,
IssueTemplateChild,
Project,
Sprint,
Team
} from '@hcengineering/tracker'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { ActionContext, FixedColumn } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import Circles from '../icons/Circles.svelte'
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
import PriorityEditor from '../issues/PriorityEditor.svelte'
import StatusEditor from '../issues/StatusEditor.svelte'
import DraftIssueChildEditor from './DraftIssueChildEditor.svelte'
import EstimationEditor from './EstimationEditor.svelte'
export let issues: DraftIssueChild[]
export let team: Ref<Team>
export let sprint: Ref<Sprint> | null = null
export let project: Ref<Project> | null = null
export let statuses: WithLookup<IssueStatus>[]
const dispatch = createEventDispatcher()
let draggingIndex: number | null = null
let hoveringIndex: number | null = null
function openIssue (evt: MouseEvent, target: DraftIssueChild) {
showPopup(
DraftIssueChildEditor,
{
showBorder: true,
team: currentTeam,
sprint,
project,
statuses,
childIssue: target
},
eventToHTMLElement(evt),
(evt: DraftIssueChild | undefined | null) => {
if (evt != null) {
const pos = issues.findIndex((it) => it.id === evt.id)
if (pos !== -1) {
issues[pos] = evt
dispatch('update-issue', evt)
}
}
}
)
}
function resetDrag () {
draggingIndex = null
hoveringIndex = null
}
function handleDragStart (ev: DragEvent, index: number) {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move'
ev.dataTransfer.dropEffect = 'move'
draggingIndex = index
}
}
function handleDrop (ev: DragEvent, toIndex: number) {
if (ev.dataTransfer && draggingIndex !== null && toIndex !== draggingIndex) {
ev.dataTransfer.dropEffect = 'move'
dispatch('move', { fromIndex: draggingIndex, toIndex })
}
resetDrag()
}
const teamQuery = createQuery()
$: teamQuery.query(
tracker.class.Team,
{
_id: team
},
(res) => ([currentTeam] = res)
)
let currentTeam: Team | undefined = undefined
function getIssueTemplateId (currentTeam: Team | undefined, issue: IssueTemplateChild): string {
return currentTeam
? `${currentTeam.identifier}-${issues.findIndex((it) => it.id === issue.id)}`
: `${issues.findIndex((it) => it.id === issue.id)}}`
}
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
{#each issues as issue, index (issue.id)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-between row"
class:is-dragging={index === draggingIndex}
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex}
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex}
animate:flip={{ duration: 400 }}
draggable={true}
on:click|self={(evt) => openIssue(evt, issue)}
on:dragstart={(ev) => handleDragStart(ev, index)}
on:dragover|preventDefault={() => false}
on:dragenter={() => (hoveringIndex = index)}
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
on:dragend={resetDrag}
>
<div class="draggable-container">
<div class="draggable-mark"><Circles /></div>
</div>
<div class="flex-row-center ml-6 clear-mins gap-2">
<StatusEditor
value={issue}
{statuses}
kind="list"
size="small"
shouldShowLabel={true}
on:change={({ detail }) => (issue.status = detail)}
/>
<PriorityEditor
value={issue}
isEditable
kind={'list'}
size={'small'}
justify={'center'}
on:change={(evt) => {
dispatch('update-issue', { id: issue.id, priority: evt.detail })
issue.priority = evt.detail
}}
/>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="issuePresenter" on:click={(evt) => openIssue(evt, issue)}>
<FixedColumn key={'issue_template_issue'} justify={'left'}>
{getIssueTemplateId(currentTeam, issue)}
</FixedColumn>
</span>
<span class="text name" title={issue.title} on:click={(evt) => openIssue(evt, issue)}>
{issue.title}
</span>
</div>
<div class="flex-center flex-no-shrink">
<EstimationEditor
kind={'link'}
size={'large'}
bind:value={issue}
on:change={(evt) => {
dispatch('update-issue', { id: issue.id, estimation: evt.detail })
issue.estimation = evt.detail
}}
/>
<AssigneeEditor
value={issue}
on:change={(evt) => {
dispatch('update-issue', { id: issue.id, assignee: evt.detail })
issue.assignee = evt.detail
}}
/>
</div>
</div>
{/each}
<style lang="scss">
.row {
position: relative;
border-bottom: 1px solid var(--divider-color);
.text {
font-weight: 500;
color: var(--caption-color);
}
.issuePresenter {
flex-shrink: 0;
min-width: 0;
min-height: 0;
font-weight: 500;
color: var(--content-color);
cursor: pointer;
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
&:active {
color: var(--accent-color);
}
}
.name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.draggable-container {
position: absolute;
display: flex;
align-items: center;
height: 100%;
width: 1.5rem;
cursor: grabbing;
.draggable-mark {
opacity: 0;
width: 0.375rem;
height: 1rem;
margin-left: 0.75rem;
transition: opacity 0.1s;
}
}
&:hover {
.draggable-mark {
opacity: 0.4;
}
}
&.is-dragging::before {
position: absolute;
content: '';
background-color: var(--theme-bg-color);
opacity: 0.4;
inset: 0;
}
&.is-dragged-over-up::before {
position: absolute;
content: '';
inset: 0;
border-top: 1px solid var(--theme-bg-check);
}
&.is-dragged-over-down::before {
position: absolute;
content: '';
inset: 0;
border-bottom: 1px solid var(--theme-bg-check);
}
}
</style>

View File

@ -13,15 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentDocList, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { AttachmentDocList } from '@hcengineering/attachment-resources'
import { Class, Data, Doc, Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import type { IssueTemplate, IssueTemplateChild, Team } from '@hcengineering/tracker'
import tags from '@hcengineering/tags'
import type { IssueTemplate, IssueTemplateChild, Team } from '@hcengineering/tracker'
import {
Button,
EditBox,
@ -38,6 +38,7 @@
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import tracker from '../../plugin'
import { StyledTextBox } from '@hcengineering/text-editor'
import SubIssueTemplates from './IssueTemplateChilds.svelte'
import TemplateControlPanel from './TemplateControlPanel.svelte'
@ -56,7 +57,7 @@
let description = ''
let innerWidth: number
let isEditing = false
let descriptionBox: AttachmentStyledBox
let descriptionBox: StyledTextBox
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
@ -213,11 +214,8 @@
/>
<div class="flex-between mt-6">
<div class="flex-grow">
<AttachmentStyledBox
<StyledTextBox
bind:this={descriptionBox}
objectId={_id}
_class={tracker.class.Issue}
space={template.space}
alwaysEdit
showButtons
maxHeight={'card'}
@ -255,6 +253,7 @@
{#if currentTeam !== undefined}
<SubIssueTemplates
maxHeight="limited"
team={template.space}
bind:children={template.children}
on:create-issue={createIssue}
on:update-issue={updateIssue}

View File

@ -14,7 +14,8 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IssueTemplateChild, Project, Sprint } from '@hcengineering/tracker'
import { createQuery } from '@hcengineering/presentation'
import tracker, { IssueTemplateChild, Project, Sprint, Team } from '@hcengineering/tracker'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { ActionContext, FixedColumn } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
@ -26,11 +27,9 @@
import IssueTemplateChildEditor from './IssueTemplateChildEditor.svelte'
export let issues: IssueTemplateChild[]
export let team: Ref<Team>
export let sprint: Ref<Sprint> | null = null
export let project: Ref<Project> | null = null
// export let template: IssueTemplate | Data<IssueTemplate>
export let teamId: string
const dispatch = createEventDispatcher()
@ -82,8 +81,20 @@
resetDrag()
}
export function getIssueTemplateId (team: string, issue: IssueTemplateChild): string {
return `${team}-${issues.findIndex((it) => it.id === issue.id)}`
const teamQuery = createQuery()
$: teamQuery.query(
tracker.class.Team,
{
_id: team
},
(res) => ([currentTeam] = res)
)
let currentTeam: Team | undefined = undefined
function getIssueTemplateId (currentTeam: Team | undefined, issue: IssueTemplateChild): string {
return currentTeam
? `${currentTeam.identifier}-${issues.findIndex((it) => it.id === issue.id)}`
: `${issues.findIndex((it) => it.id === issue.id)}}`
}
</script>
@ -127,7 +138,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="issuePresenter" on:click={(evt) => openIssue(evt, issue)}>
<FixedColumn key={'issue_template_issue'} justify={'left'}>
{getIssueTemplateId(teamId, issue)}
{getIssueTemplateId(currentTeam, issue)}
</FixedColumn>
</span>
<span class="text name" title={issue.title} on:click={(evt) => openIssue(evt, issue)}>

View File

@ -14,10 +14,9 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IssueTemplateChild, Project, Sprint } from '@hcengineering/tracker'
import { IssueTemplateChild, Project, Sprint, Team } from '@hcengineering/tracker'
import { Button, closeTooltip, ExpandCollapse, IconAdd, Scroller } from '@hcengineering/ui'
import { afterUpdate } from 'svelte'
import { createEventDispatcher } from 'svelte'
import { afterUpdate, createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import Collapsed from '../icons/Collapsed.svelte'
import Expanded from '../icons/Expanded.svelte'
@ -25,7 +24,7 @@
import IssueTemplateChildList from './IssueTemplateChildList.svelte'
export let children: IssueTemplateChild[] = []
export let teamId: string = 'TSK'
export let team: Ref<Team>
export let sprint: Ref<Sprint> | null = null
export let project: Ref<Project> | null = null
export let isScrollable: boolean = false
@ -92,7 +91,7 @@
{project}
{sprint}
bind:issues={children}
{teamId}
{team}
on:move={handleIssueSwap}
on:update-issue
/>

View File

@ -221,7 +221,7 @@ export interface IssueDraft extends Doc {
parentIssue?: string
attachments?: number
labels?: TagReference[]
subIssues?: IssueTemplateChild[]
subIssues?: DraftIssueChild[]
template?: {
// A template issue is based on
template: Ref<IssueTemplate>
@ -253,7 +253,7 @@ export interface IssueTemplateData {
* @public
*/
export interface IssueTemplateChild extends IssueTemplateData {
id: string
id: Ref<Issue>
}
/**
@ -271,6 +271,13 @@ export interface IssueTemplate extends Doc, IssueTemplateData {
relations?: RelatedDocument[]
}
/**
* @public
*/
export interface DraftIssueChild extends IssueTemplateChild {
status: Ref<IssueStatus>
}
/**
* @public
*