mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-12 05:27:56 +00:00
Subissues with attachments (#2641)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
54e62d9547
commit
786e51c6e6
@ -40,12 +40,12 @@
|
|||||||
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
||||||
import {
|
import {
|
||||||
calcRank,
|
calcRank,
|
||||||
|
DraftIssueChild,
|
||||||
Issue,
|
Issue,
|
||||||
IssueDraft,
|
IssueDraft,
|
||||||
IssuePriority,
|
IssuePriority,
|
||||||
IssueStatus,
|
IssueStatus,
|
||||||
IssueTemplate,
|
IssueTemplate,
|
||||||
IssueTemplateChild,
|
|
||||||
Project,
|
Project,
|
||||||
Sprint,
|
Sprint,
|
||||||
Team
|
Team
|
||||||
@ -80,7 +80,7 @@
|
|||||||
import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
|
import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
|
||||||
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
|
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
|
||||||
import SprintSelector from './sprints/SprintSelector.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 space: Ref<Team>
|
||||||
export let status: Ref<IssueStatus> | undefined = undefined
|
export let status: Ref<IssueStatus> | undefined = undefined
|
||||||
@ -96,6 +96,8 @@
|
|||||||
|
|
||||||
const draft: IssueDraft | undefined = shouldSaveDraft ? getUserDraft(tracker.class.IssueDraft) : undefined
|
const draft: IssueDraft | undefined = shouldSaveDraft ? getUserDraft(tracker.class.IssueDraft) : undefined
|
||||||
|
|
||||||
|
let subIssuesComponent: SubIssues
|
||||||
|
|
||||||
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||||
let labels: TagReference[] = draft?.labels || []
|
let labels: TagReference[] = draft?.labels || []
|
||||||
let objectId: Ref<Issue> = draft?.issueId || generateId()
|
let objectId: Ref<Issue> = draft?.issueId || generateId()
|
||||||
@ -147,9 +149,8 @@
|
|||||||
templateId = undefined
|
templateId = undefined
|
||||||
template = undefined
|
template = undefined
|
||||||
object = { ...defaultIssue }
|
object = { ...defaultIssue }
|
||||||
subIssues = []
|
|
||||||
if (!originalIssue && !draft) {
|
if (!originalIssue && !draft) {
|
||||||
updateIssueStatusId(_space, status)
|
updateIssueStatusId(currentTeam, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +159,7 @@
|
|||||||
let template: IssueTemplate | undefined = undefined
|
let template: IssueTemplate | undefined = undefined
|
||||||
const templateQuery = createQuery()
|
const templateQuery = createQuery()
|
||||||
|
|
||||||
let subIssues: IssueTemplateChild[] = draft?.subIssues || []
|
let subIssues: DraftIssueChild[] = draft?.subIssues || []
|
||||||
|
|
||||||
$: if (templateId !== undefined) {
|
$: if (templateId !== undefined) {
|
||||||
templateQuery.query(tracker.class.IssueTemplate, { _id: templateId }, (res) => {
|
templateQuery.query(tracker.class.IssueTemplate, { _id: templateId }, (res) => {
|
||||||
@ -192,7 +193,9 @@
|
|||||||
|
|
||||||
const { _class, _id, space, children, comments, attachments, labels: labels_, ...templBase } = template
|
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 = {
|
||||||
...object,
|
...object,
|
||||||
@ -226,7 +229,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: _space = draft?.team || space
|
$: _space = draft?.team || space
|
||||||
$: !originalIssue && !draft && updateIssueStatusId(_space, status)
|
$: !originalIssue && !draft && updateIssueStatusId(currentTeam, status)
|
||||||
$: canSave = getTitle(object.title ?? '').length > 0
|
$: canSave = getTitle(object.title ?? '').length > 0
|
||||||
|
|
||||||
$: statusesQuery.query(
|
$: 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) {
|
if (issueStatusId !== undefined) {
|
||||||
object.status = issueStatusId
|
object.status = issueStatusId
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const team = await client.findOne(tracker.class.Team, { _id: teamId })
|
if (currentTeam?.defaultIssueStatus) {
|
||||||
|
object.status = currentTeam.defaultIssueStatus
|
||||||
if (team?.defaultIssueStatus) {
|
|
||||||
object.status = team.defaultIssueStatus
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,9 +343,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (object.attachments && object.attachments > 0) {
|
if (object.attachments && object.attachments > 0) {
|
||||||
// return false
|
return false
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (draft.project && draft.project !== defaultIssue.project) {
|
if (draft.project && draft.project !== defaultIssue.project) {
|
||||||
return false
|
return false
|
||||||
@ -358,10 +359,8 @@
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const team = await client.findOne(tracker.class.Team, { _id: _space })
|
if (currentTeam?.defaultIssueStatus) {
|
||||||
|
return draft.status === currentTeam.defaultIssueStatus
|
||||||
if (team?.defaultIssueStatus) {
|
|
||||||
return draft.status === team.defaultIssueStatus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -470,66 +469,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const subIssue of subIssues) {
|
const parents = parentIssue
|
||||||
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
|
? [
|
||||||
const incResult = await client.updateDoc(
|
{ parentId: objectId, parentTitle: value.title },
|
||||||
tracker.class.Team,
|
{ parentId: parentIssue._id, parentTitle: parentIssue.title },
|
||||||
core.space.Space,
|
...parentIssue.parents
|
||||||
_space,
|
]
|
||||||
{
|
: [{ parentId: objectId, parentTitle: value.title }]
|
||||||
$inc: { sequence: 1 }
|
await subIssuesComponent.save(parents)
|
||||||
},
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addNotification(await translate(tracker.string.IssueCreated, {}), getTitle(object.title), IssueNotification, {
|
addNotification(await translate(tracker.string.IssueCreated, {}), getTitle(object.title), IssueNotification, {
|
||||||
issueId: objectId,
|
issueId: objectId,
|
||||||
subTitlePostfix: (await translate(tracker.string.Created, { value: 1 })).toLowerCase(),
|
subTitlePostfix: (await translate(tracker.string.Created, { value: 1 })).toLowerCase(),
|
||||||
@ -677,14 +624,6 @@
|
|||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space={_space} />
|
<SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space={_space} />
|
||||||
</div>
|
</div>
|
||||||
<!-- <Button
|
|
||||||
icon={tracker.icon.Home}
|
|
||||||
label={presentation.string.Save}
|
|
||||||
size={'small'}
|
|
||||||
kind={'no-border'}
|
|
||||||
disabled
|
|
||||||
on:click={() => {}}
|
|
||||||
/> -->
|
|
||||||
<ObjectBox
|
<ObjectBox
|
||||||
_class={tracker.class.IssueTemplate}
|
_class={tracker.class.IssueTemplate}
|
||||||
value={templateId}
|
value={templateId}
|
||||||
@ -741,13 +680,17 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
<IssueTemplateChilds
|
{#if issueStatuses}
|
||||||
bind:children={subIssues}
|
<SubIssues
|
||||||
sprint={object.sprint}
|
bind:this={subIssuesComponent}
|
||||||
project={object.project}
|
teamId={_space}
|
||||||
isScrollable
|
parent={objectId}
|
||||||
maxHeight="limited"
|
statuses={issueStatuses ?? []}
|
||||||
/>
|
team={currentTeam}
|
||||||
|
sprint={object.sprint}
|
||||||
|
project={object.project}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<svelte:fragment slot="pool">
|
<svelte:fragment slot="pool">
|
||||||
{#if issueStatuses}
|
{#if issueStatuses}
|
||||||
<div id="status-editor">
|
<div id="status-editor">
|
||||||
|
252
plugins/tracker-resources/src/components/SubIssues.svelte
Normal file
252
plugins/tracker-resources/src/components/SubIssues.svelte
Normal 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>
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
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 type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||||
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
|
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
@ -23,7 +23,7 @@
|
|||||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||||
import StatusPresenter from './StatusPresenter.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 statuses: WithLookup<IssueStatus>[] | undefined = undefined
|
||||||
export let isEditable: boolean = true
|
export let isEditable: boolean = true
|
||||||
export let shouldShowLabel: boolean = false
|
export let shouldShowLabel: boolean = false
|
||||||
|
@ -13,13 +13,13 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
|
||||||
import { Employee } from '@hcengineering/contact'
|
import { Employee } from '@hcengineering/contact'
|
||||||
import { Data, Doc, generateId, Ref } from '@hcengineering/core'
|
import { Data, Doc, generateId, Ref } from '@hcengineering/core'
|
||||||
import { Card, getClient, KeyedAttribute, SpaceSelector } from '@hcengineering/presentation'
|
import { Card, getClient, KeyedAttribute, SpaceSelector } from '@hcengineering/presentation'
|
||||||
import tags, { TagElement } from '@hcengineering/tags'
|
import tags, { TagElement } from '@hcengineering/tags'
|
||||||
|
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||||
import { IssuePriority, IssueTemplate, Project, Sprint, Team } from '@hcengineering/tracker'
|
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 { createEventDispatcher } from 'svelte'
|
||||||
import { activeProject, activeSprint } from '../../issues'
|
import { activeProject, activeSprint } from '../../issues'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
@ -58,7 +58,7 @@
|
|||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
let descriptionBox: AttachmentStyledBox
|
let descriptionBox: StyledTextBox
|
||||||
|
|
||||||
const key: KeyedAttribute = {
|
const key: KeyedAttribute = {
|
||||||
key: 'labels',
|
key: 'labels',
|
||||||
@ -150,11 +150,8 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<EditBox bind:value={object.title} placeholder={tracker.string.IssueTitlePlaceholder} kind={'large-style'} focus />
|
<EditBox bind:value={object.title} placeholder={tracker.string.IssueTitlePlaceholder} kind={'large-style'} focus />
|
||||||
<AttachmentStyledBox
|
<StyledTextBox
|
||||||
bind:this={descriptionBox}
|
bind:this={descriptionBox}
|
||||||
{objectId}
|
|
||||||
_class={tracker.class.Issue}
|
|
||||||
space={_space}
|
|
||||||
alwaysEdit
|
alwaysEdit
|
||||||
showButtons={false}
|
showButtons={false}
|
||||||
emphasized
|
emphasized
|
||||||
@ -165,7 +162,7 @@
|
|||||||
bind:children={object.children}
|
bind:children={object.children}
|
||||||
project={object.project}
|
project={object.project}
|
||||||
sprint={object.sprint}
|
sprint={object.sprint}
|
||||||
teamId={spaceRef?.identifier ?? 'TSK'}
|
team={_space}
|
||||||
maxHeight="limited"
|
maxHeight="limited"
|
||||||
/>
|
/>
|
||||||
<svelte:fragment slot="pool">
|
<svelte:fragment slot="pool">
|
||||||
@ -204,13 +201,4 @@
|
|||||||
<ProjectSelector value={object.project} onChange={handleProjectIdChanged} />
|
<ProjectSelector value={object.project} onChange={handleProjectIdChanged} />
|
||||||
<SprintSelector value={object.sprint} onChange={handleSprintIdChanged} useProject={object.project ?? undefined} />
|
<SprintSelector value={object.sprint} onChange={handleSprintIdChanged} useProject={object.project ?? undefined} />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="footer">
|
|
||||||
<Button
|
|
||||||
icon={IconAttachment}
|
|
||||||
kind={'transparent'}
|
|
||||||
on:click={() => {
|
|
||||||
descriptionBox.attach()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</svelte:fragment>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -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>
|
@ -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>
|
@ -13,15 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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 { Class, Data, Doc, Ref, WithLookup } from '@hcengineering/core'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { Panel } from '@hcengineering/panel'
|
import { Panel } from '@hcengineering/panel'
|
||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||||
import setting, { settingId } from '@hcengineering/setting'
|
import setting, { settingId } from '@hcengineering/setting'
|
||||||
import type { IssueTemplate, IssueTemplateChild, Team } from '@hcengineering/tracker'
|
|
||||||
import tags from '@hcengineering/tags'
|
import tags from '@hcengineering/tags'
|
||||||
|
import type { IssueTemplate, IssueTemplateChild, Team } from '@hcengineering/tracker'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
EditBox,
|
EditBox,
|
||||||
@ -38,6 +38,7 @@
|
|||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
|
||||||
|
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||||
import SubIssueTemplates from './IssueTemplateChilds.svelte'
|
import SubIssueTemplates from './IssueTemplateChilds.svelte'
|
||||||
import TemplateControlPanel from './TemplateControlPanel.svelte'
|
import TemplateControlPanel from './TemplateControlPanel.svelte'
|
||||||
|
|
||||||
@ -56,7 +57,7 @@
|
|||||||
let description = ''
|
let description = ''
|
||||||
let innerWidth: number
|
let innerWidth: number
|
||||||
let isEditing = false
|
let isEditing = false
|
||||||
let descriptionBox: AttachmentStyledBox
|
let descriptionBox: StyledTextBox
|
||||||
|
|
||||||
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
|
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
|
||||||
|
|
||||||
@ -213,11 +214,8 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex-between mt-6">
|
<div class="flex-between mt-6">
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<AttachmentStyledBox
|
<StyledTextBox
|
||||||
bind:this={descriptionBox}
|
bind:this={descriptionBox}
|
||||||
objectId={_id}
|
|
||||||
_class={tracker.class.Issue}
|
|
||||||
space={template.space}
|
|
||||||
alwaysEdit
|
alwaysEdit
|
||||||
showButtons
|
showButtons
|
||||||
maxHeight={'card'}
|
maxHeight={'card'}
|
||||||
@ -255,6 +253,7 @@
|
|||||||
{#if currentTeam !== undefined}
|
{#if currentTeam !== undefined}
|
||||||
<SubIssueTemplates
|
<SubIssueTemplates
|
||||||
maxHeight="limited"
|
maxHeight="limited"
|
||||||
|
team={template.space}
|
||||||
bind:children={template.children}
|
bind:children={template.children}
|
||||||
on:create-issue={createIssue}
|
on:create-issue={createIssue}
|
||||||
on:update-issue={updateIssue}
|
on:update-issue={updateIssue}
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref } from '@hcengineering/core'
|
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 { eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||||
import { ActionContext, FixedColumn } from '@hcengineering/view-resources'
|
import { ActionContext, FixedColumn } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
@ -26,11 +27,9 @@
|
|||||||
import IssueTemplateChildEditor from './IssueTemplateChildEditor.svelte'
|
import IssueTemplateChildEditor from './IssueTemplateChildEditor.svelte'
|
||||||
|
|
||||||
export let issues: IssueTemplateChild[]
|
export let issues: IssueTemplateChild[]
|
||||||
|
export let team: Ref<Team>
|
||||||
export let sprint: Ref<Sprint> | null = null
|
export let sprint: Ref<Sprint> | null = null
|
||||||
export let project: Ref<Project> | null = null
|
export let project: Ref<Project> | null = null
|
||||||
// export let template: IssueTemplate | Data<IssueTemplate>
|
|
||||||
|
|
||||||
export let teamId: string
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
@ -82,8 +81,20 @@
|
|||||||
resetDrag()
|
resetDrag()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIssueTemplateId (team: string, issue: IssueTemplateChild): string {
|
const teamQuery = createQuery()
|
||||||
return `${team}-${issues.findIndex((it) => it.id === issue.id)}`
|
$: 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>
|
</script>
|
||||||
|
|
||||||
@ -127,7 +138,7 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<span class="issuePresenter" on:click={(evt) => openIssue(evt, issue)}>
|
<span class="issuePresenter" on:click={(evt) => openIssue(evt, issue)}>
|
||||||
<FixedColumn key={'issue_template_issue'} justify={'left'}>
|
<FixedColumn key={'issue_template_issue'} justify={'left'}>
|
||||||
{getIssueTemplateId(teamId, issue)}
|
{getIssueTemplateId(currentTeam, issue)}
|
||||||
</FixedColumn>
|
</FixedColumn>
|
||||||
</span>
|
</span>
|
||||||
<span class="text name" title={issue.title} on:click={(evt) => openIssue(evt, issue)}>
|
<span class="text name" title={issue.title} on:click={(evt) => openIssue(evt, issue)}>
|
||||||
|
@ -14,10 +14,9 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Ref } from '@hcengineering/core'
|
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 { Button, closeTooltip, ExpandCollapse, IconAdd, Scroller } from '@hcengineering/ui'
|
||||||
import { afterUpdate } from 'svelte'
|
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import Collapsed from '../icons/Collapsed.svelte'
|
import Collapsed from '../icons/Collapsed.svelte'
|
||||||
import Expanded from '../icons/Expanded.svelte'
|
import Expanded from '../icons/Expanded.svelte'
|
||||||
@ -25,7 +24,7 @@
|
|||||||
import IssueTemplateChildList from './IssueTemplateChildList.svelte'
|
import IssueTemplateChildList from './IssueTemplateChildList.svelte'
|
||||||
|
|
||||||
export let children: IssueTemplateChild[] = []
|
export let children: IssueTemplateChild[] = []
|
||||||
export let teamId: string = 'TSK'
|
export let team: Ref<Team>
|
||||||
export let sprint: Ref<Sprint> | null = null
|
export let sprint: Ref<Sprint> | null = null
|
||||||
export let project: Ref<Project> | null = null
|
export let project: Ref<Project> | null = null
|
||||||
export let isScrollable: boolean = false
|
export let isScrollable: boolean = false
|
||||||
@ -92,7 +91,7 @@
|
|||||||
{project}
|
{project}
|
||||||
{sprint}
|
{sprint}
|
||||||
bind:issues={children}
|
bind:issues={children}
|
||||||
{teamId}
|
{team}
|
||||||
on:move={handleIssueSwap}
|
on:move={handleIssueSwap}
|
||||||
on:update-issue
|
on:update-issue
|
||||||
/>
|
/>
|
||||||
|
@ -221,7 +221,7 @@ export interface IssueDraft extends Doc {
|
|||||||
parentIssue?: string
|
parentIssue?: string
|
||||||
attachments?: number
|
attachments?: number
|
||||||
labels?: TagReference[]
|
labels?: TagReference[]
|
||||||
subIssues?: IssueTemplateChild[]
|
subIssues?: DraftIssueChild[]
|
||||||
template?: {
|
template?: {
|
||||||
// A template issue is based on
|
// A template issue is based on
|
||||||
template: Ref<IssueTemplate>
|
template: Ref<IssueTemplate>
|
||||||
@ -253,7 +253,7 @@ export interface IssueTemplateData {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface IssueTemplateChild extends IssueTemplateData {
|
export interface IssueTemplateChild extends IssueTemplateData {
|
||||||
id: string
|
id: Ref<Issue>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -271,6 +271,13 @@ export interface IssueTemplate extends Doc, IssueTemplateData {
|
|||||||
relations?: RelatedDocument[]
|
relations?: RelatedDocument[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface DraftIssueChild extends IssueTemplateChild {
|
||||||
|
status: Ref<IssueStatus>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
|
Loading…
Reference in New Issue
Block a user