Use list for sub issues (#2514)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2023-01-18 12:14:59 +07:00
committed by GitHub
parent 1e14668439
commit 28f3e6169c
80 changed files with 706 additions and 518 deletions
@@ -241,6 +241,7 @@
<span class="title select-text">{title}</span>
<div class="mt-6 description-preview select-text">
{#if isDescriptionEmpty}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="placeholder" on:click={edit}>
<Label label={tracker.string.IssueDescriptionPlaceholder} />
</div>
@@ -13,65 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, WithLookup } from '@hcengineering/core'
import { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { getEventPositionElement, showPanel, showPopup } from '@hcengineering/ui'
import { ActionContext, ContextMenu, FixedColumn } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import { getIssueId } from '../../../issues'
import { Viewlet, ViewOptions } from '@hcengineering/view'
import { ActionContext, List } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import { subIssueListProvider } from '../../../utils'
import Circles from '../../icons/Circles.svelte'
import AssigneeEditor from '../AssigneeEditor.svelte'
import DueDateEditor from '../DueDateEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
import SubIssuesSelector from './SubIssuesSelector.svelte'
export let issues: Issue[]
export let query: DocumentQuery<Issue> | undefined = undefined
export let issues: Issue[] | undefined = undefined
export let viewlet: Viewlet
export let viewOptions: ViewOptions
export let teams: Map<Ref<Team>, Team>
// Extra properties
export let teams: Map<Ref<Team>, Team> | undefined
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const dispatch = createEventDispatcher()
let draggingIndex: number | null = null
let hoveringIndex: number | null = null
function openIssue (target: Issue) {
dispatch('issue-focus', target)
subIssueListProvider(issues, target._id)
showPanel(tracker.component.EditIssue, target._id, target._class, 'content')
}
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()
}
function showContextMenu (ev: MouseEvent, object: Issue) {
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
}
</script>
<ActionContext
@@ -80,136 +35,15 @@
}}
/>
{#each issues as issue, index (issue._id)}
{@const currentTeam = teams.get(issue.space)}
{@const openIssueCall = () => openIssue(issue)}
<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={openIssueCall}
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, 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">
<PriorityEditor value={issue} isEditable kind={'list'} size={'small'} justify={'center'} />
<span class="issuePresenter" on:click={openIssueCall}>
<FixedColumn key={'subissue_issue'} justify={'left'}>
{#if currentTeam}
{getIssueId(currentTeam, issue)}
{/if}
</FixedColumn>
</span>
<StatusEditor
value={issue}
statuses={issueStatuses.get(issue.space)}
justify="center"
kind={'list'}
size={'small'}
tooltipAlignment="bottom"
/>
<span class="text name" title={issue.title} on:click={openIssueCall}>
{issue.title}
</span>
{#if issue.subIssues > 0}
<SubIssuesSelector value={issue} {currentTeam} />
{/if}
</div>
<div class="flex-center flex-no-shrink">
<EstimationEditor value={issue} kind={'list'} />
{#if issue.dueDate !== null}
<DueDateEditor value={issue} />
{/if}
<AssigneeEditor value={issue} />
</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>
{#if viewlet}
<List
_class={tracker.class.Issue}
{viewOptions}
viewOptionsConfig={viewlet.viewOptions?.other}
config={viewlet.config}
documents={issues}
{query}
flatHeaders={true}
props={{ teams, issueStatuses }}
/>
{/if}
@@ -92,7 +92,7 @@
$: areSubIssuesLoading = !subIssues
$: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null
$: if (parentIssue) {
$: if (parentIssue && parentIssue.subIssues > 0) {
subIssuesQeury.query(
tracker.class.Issue,
{ space: issue.space, attachedTo: parentIssue._id },
@@ -113,6 +113,7 @@
{#if parentIssue}
<div class="flex root">
<div class="item clear-mins">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-center parent-issue cursor-pointer"
use:tooltip={{ label: tracker.string.OpenParent, direction: 'bottom' }}
@@ -136,6 +137,7 @@
<Spinner size="small" />
</div>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={subIssuesElement}
class="flex-center sub-issues cursor-pointer"
@@ -14,9 +14,11 @@
-->
<script lang="ts">
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd, Chevron, Label } from '@hcengineering/ui'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
import { Button, Chevron, closeTooltip, ExpandCollapse, IconAdd, Label } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import CreateSubIssue from './CreateSubIssue.svelte'
import SubIssueList from './SubIssueList.svelte'
@@ -25,35 +27,54 @@
export let teams: Map<Ref<Team>, Team>
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
const subIssuesQuery = createQuery()
const client = getClient()
let subIssues: Issue[] | undefined
let isCollapsed = false
let isCreating = false
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
if (subIssues) {
const { fromIndex, toIndex } = ev.detail
const [prev, next] = [
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
]
const issue = subIssues[fromIndex]
$: hasSubIssues = issue.subIssues > 0
await client.update(issue, { rank: calcRank(prev, next) })
}
let viewlet: Viewlet | undefined
const query = createQuery()
$: query.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
;[viewlet] = res
})
let _teams = teams
let _issueStatuses = issueStatuses
const teamsQuery = createQuery()
$: if (teams === undefined) {
teamsQuery.query(tracker.class.Team, {}, async (result) => {
_teams = new Map(result.map((it) => [it._id, it]))
})
} else {
teamsQuery.unsubscribe()
}
$: hasSubIssues = issue.subIssues > 0
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
sort: { rank: SortingOrder.Ascending },
lookup: {
_id: {
subIssues: tracker.class.Issue
const statusesQuery = createQuery()
$: if (issueStatuses === undefined) {
statusesQuery.query(
tracker.class.IssueStatus,
{},
(statuses) => {
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
for (const s of statuses) {
const id = s.attachedTo as Ref<Team>
st.set(id, [...(st.get(id) ?? []), s])
}
_issueStatuses = st
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
}
})
)
} else {
statusesQuery.unsubscribe()
}
$: viewOptions = viewlet !== undefined ? getViewOptions(viewlet) : undefined
</script>
<div class="flex-between">
@@ -73,38 +94,42 @@
</svelte:fragment>
</Button>
{/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 }, direction: 'bottom' }}
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
<div class="flex-row-center">
{#if viewlet && hasSubIssues && viewOptions}
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
{/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 }, direction: 'bottom' }}
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
</div>
</div>
<div class="mt-1">
{#if subIssues && issueStatuses}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if hasSubIssues}
{#if issueStatuses}
{#if hasSubIssues && viewOptions && viewlet}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
<div class="list" class:collapsed={isCollapsed}>
<SubIssueList
issues={subIssues}
{issueStatuses}
{teams}
on:issue-focus={() => (isCreating = false)}
on:move={handleIssueSwap}
teams={_teams}
{viewlet}
{viewOptions}
issueStatuses={_issueStatuses}
query={{ attachedTo: issue._id }}
/>
</div>
{/if}
</ExpandCollapse>
</ExpandCollapse>
{/if}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if isCreating}
{@const team = teams.get(issue.space)}
@@ -121,10 +146,6 @@
{/if}
{/if}
</ExpandCollapse>
{:else}
<div class="flex-center pt-3">
<Spinner />
</div>
{/if}
</div>