mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 21:50:34 +03: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 {
|
||||
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
|
||||
const 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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
: [{ 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}
|
||||
{#if issueStatuses}
|
||||
<SubIssues
|
||||
bind:this={subIssuesComponent}
|
||||
teamId={_space}
|
||||
parent={objectId}
|
||||
statuses={issueStatuses ?? []}
|
||||
team={currentTeam}
|
||||
sprint={object.sprint}
|
||||
project={object.project}
|
||||
isScrollable
|
||||
maxHeight="limited"
|
||||
/>
|
||||
{/if}
|
||||
<svelte:fragment slot="pool">
|
||||
{#if issueStatuses}
|
||||
<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">
|
||||
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
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
-->
|
||||
<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}
|
||||
|
@ -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)}>
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user