Tracker: add Sub-issues list (#1989)

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
Sergei Ogorelkov 2022-06-02 23:57:07 +07:00 committed by GitHub
parent b1506d1a65
commit 30a9186eab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 340 additions and 5 deletions

View File

@ -0,0 +1,45 @@
<!--
// 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 { quintOut } from 'svelte/easing'
import { tweened } from 'svelte/motion'
import { syncHeight } from '../utils'
export let isExpanded = false
export let duration = 200
export let easing: (t: number) => number = quintOut
const tweenedHeight = tweened(0, {
duration: duration,
easing
})
let element: HTMLDivElement
$: height = syncHeight(element)
$: tweenedHeight.set(isExpanded ? $height : 0, { duration, easing })
</script>
<div class="root" style="height: {$tweenedHeight}px">
<div bind:this={element}>
<slot />
</div>
</div>
<style lang="scss">
.root {
overflow: hidden;
}
</style>

View File

@ -141,6 +141,7 @@ export { default as WeekCalendar } from './components/calendar/WeekCalendar.svel
export { default as FocusHandler } from './components/FocusHandler.svelte' export { default as FocusHandler } from './components/FocusHandler.svelte'
export { default as ListView } from './components/ListView.svelte' export { default as ListView } from './components/ListView.svelte'
export { default as ToggleButton } from './components/ToggleButton.svelte' export { default as ToggleButton } from './components/ToggleButton.svelte'
export { default as ExpandCollapse } from './components/ExpandCollapse.svelte'
export * from './types' export * from './types'
export * from './location' export * from './location'

View File

@ -16,6 +16,7 @@
// import type { Metadata } from '@anticrm/platform' // import type { Metadata } from '@anticrm/platform'
import type { Metadata } from '@anticrm/platform' import type { Metadata } from '@anticrm/platform'
import { setMetadata } from '@anticrm/platform' import { setMetadata } from '@anticrm/platform'
import { Writable, writable } from 'svelte/store'
export function setMetadataLocalStorage (id: Metadata<string>, value: string | null): void { export function setMetadataLocalStorage (id: Metadata<string>, value: string | null): void {
if (value != null) { if (value != null) {
@ -33,3 +34,14 @@ export function fetchMetadataLocalStorage (id: Metadata<string>): string | null
} }
return value return value
} }
export function syncHeight (element: HTMLElement | undefined): Writable<number> {
return writable(0, (set) => {
if (element != null) {
const observer = new ResizeObserver(() => set(element.offsetHeight))
observer.observe(element)
return () => observer.disconnect()
}
})
}

View File

@ -69,7 +69,9 @@
"ChangeParent": "Change parent issue\u2026", "ChangeParent": "Change parent issue\u2026",
"RemoveParent": "Remove parent issue", "RemoveParent": "Remove parent issue",
"OpenParent": "Open parent issue", "OpenParent": "Open parent issue",
"OpenSub": "Open sub-issues", "SubIssues": "Sub-issues ({subIssues})",
"OpenSubIssues": "Open sub-issues",
"AddSubIssues": "{subIssues, plural, =1 {Add sub-issue} other {+ Add sub-issues}}",
"BlockedBy": "", "BlockedBy": "",
"RelatedTo": "", "RelatedTo": "",
"Comments": "", "Comments": "",

View File

@ -43,6 +43,9 @@
"Low": "Низкий", "Low": "Низкий",
"Unassigned": "Не назначен", "Unassigned": "Не назначен",
"AddIssueTooltip": "Добавить задачу\u2026", "AddIssueTooltip": "Добавить задачу\u2026",
"SubIssues": "Подзадачи ({subIssues})",
"OpenSubIssues": "Открыть подзадачи",
"AddSubIssues": "{subIssues, plural, =1 {Добавить подзадачу} other {+ Добавить подзадачи}}",
"Title": "Заголовок", "Title": "Заголовок",
"Description": "", "Description": "",

View File

@ -0,0 +1,16 @@
<script lang="ts">
const fill: string = 'var(--theme-caption-color)'
</script>
<svg {fill} viewBox="0 0 6 16" xmlns="http://www.w3.org/2000/svg">
<circle cx="1" cy="1" r="1" />
<circle cx="4.5" cy="1" r="1" />
<circle cx="1" cy="4.5" r="1" />
<circle cx="4.5" cy="4.5" r="1" />
<circle cx="1" cy="8" r="1" />
<circle cx="4.5" cy="8" r="1" />
<circle cx="1" cy="11.5" r="1" />
<circle cx="4.5" cy="11.5" r="1" />
<circle cx="1" cy="15" r="1" />
<circle cx="4.5" cy="15" r="1" />
</svg>

View File

@ -16,7 +16,7 @@
import { Ref, WithLookup } from '@anticrm/core' import { Ref, WithLookup } from '@anticrm/core'
import { Issue, IssueStatus } from '@anticrm/tracker' import { Issue, IssueStatus } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { Tooltip } from '@anticrm/ui' import { Tooltip, TooltipAlignment } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui' import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import tracker from '../../plugin' import tracker from '../../plugin'
import StatusSelector from '../StatusSelector.svelte' import StatusSelector from '../StatusSelector.svelte'
@ -25,6 +25,7 @@
export let statuses: WithLookup<IssueStatus>[] export let statuses: WithLookup<IssueStatus>[]
export let isEditable: boolean = true export let isEditable: boolean = true
export let shouldShowLabel: boolean = false export let shouldShowLabel: boolean = false
export let tooltipAlignment: TooltipAlignment | undefined = undefined
export let kind: ButtonKind = 'link' export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large' export let size: ButtonSize = 'large'
@ -52,7 +53,7 @@
{#if value} {#if value}
{#if isEditable} {#if isEditable}
<Tooltip label={tracker.string.SetStatus} fill> <Tooltip label={tracker.string.SetStatus} direction={tooltipAlignment} fill>
<StatusSelector <StatusSelector
{kind} {kind}
{size} {size}

View File

@ -38,6 +38,7 @@
import ControlPanel from './ControlPanel.svelte' import ControlPanel from './ControlPanel.svelte'
import CopyToClipboard from './CopyToClipboard.svelte' import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssueSelector from './SubIssueSelector.svelte' import SubIssueSelector from './SubIssueSelector.svelte'
import SubIssues from './SubIssues.svelte'
export let _id: Ref<Issue> export let _id: Ref<Issue>
export let _class: Ref<Class<Issue>> export let _class: Ref<Class<Issue>>
@ -239,6 +240,9 @@
<MessageViewer message={description} /> <MessageViewer message={description} />
{/if} {/if}
</div> </div>
<div class="mt-6">
<SubIssues {issue} {issueStatuses} />
</div>
{/if} {/if}
<AttachmentDocList value={issue} /> <AttachmentDocList value={issue} />

View File

@ -0,0 +1,142 @@
<!--
// 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 { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import { WithLookup } from '@anticrm/core'
import { Issue, IssueStatus } from '@anticrm/tracker'
import { showPanel } from '@anticrm/ui'
import tracker from '../../../plugin'
import ProjectEditor from '../../projects/ProjectEditor.svelte'
import AssigneeEditor from '../AssigneeEditor.svelte'
import DueDateEditor from '../DueDateEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import Circles from '../../icons/Circles.svelte'
export let issues: Issue[]
export let issueStatuses: WithLookup<IssueStatus>[]
const dispatch = createEventDispatcher()
let draggingIndex: number | null = null
let hoveringIndex: number | null = null
function openIssue (target: Issue) {
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()
}
</script>
{#each issues as issue, index (issue._id)}
<div
class="flex-between row"
class:is-dragging={index === draggingIndex}
class:is-dragged-over={index === hoveringIndex}
animate:flip={{ duration: 400 }}
draggable={true}
on:click|self={() => openIssue(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-center ml-6">
<StatusEditor value={issue} statuses={issueStatuses} kind="transparent" tooltipAlignment="bottom" />
<span class="flex-no-shrink name" on:click={() => openIssue(issue)}>
{issue.title}
</span>
</div>
<div class="flex-center">
<ProjectEditor value={issue} />
<DueDateEditor value={issue} />
<AssigneeEditor value={issue} />
</div>
</div>
{/each}
<style lang="scss">
.row {
position: relative;
border-bottom: 1px solid var(--divider-color);
.name {
font-weight: 500;
color: var(--theme-caption-color);
}
.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: '';
color: var(--theme-bg-color);
opacity: 0.4;
inset: 0;
}
}
&.is-dragged-over {
border-bottom: 1px solid var(--theme-bg-check);
}
}
</style>

View File

@ -121,7 +121,7 @@
<Spinner size="small" /> <Spinner size="small" />
</div> </div>
{:else} {:else}
<Tooltip label={tracker.string.OpenSub} direction="bottom"> <Tooltip label={tracker.string.OpenSubIssues} direction="bottom">
<div <div
bind:this={subIssuesElement} bind:this={subIssuesElement}
class="flex-center sub-issues" class="flex-center sub-issues"

View File

@ -0,0 +1,108 @@
<!--
// 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 { SortingOrder, WithLookup } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { calcRank, Issue, IssueStatus } from '@anticrm/tracker'
import { Button, Spinner, ExpandCollapse } from '@anticrm/ui'
import tracker from '../../../plugin'
import Collapsed from '../../icons/Collapsed.svelte'
import Expanded from '../../icons/Expanded.svelte'
import SubIssueList from './SubIssueList.svelte'
export let issue: Issue
export let issueStatuses: WithLookup<IssueStatus>[] | undefined
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
console.log('index', fromIndex, toIndex)
const [prev, next] = [
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
]
const issue = subIssues[fromIndex]
await client.update(issue, { rank: calcRank(prev, next) })
}
}
$: hasSubIssues = issue.subIssues > 0
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
sort: { rank: SortingOrder.Ascending }
})
</script>
<div class="flex-between">
{#if hasSubIssues}
<Button
width="min-content"
icon={isCollapsed ? Collapsed : Expanded}
size="small"
kind="transparent"
label={tracker.string.SubIssues}
labelParams={{ subIssues: issue.subIssues }}
on:click={() => (isCollapsed = !isCollapsed)}
/>
{/if}
<!-- <Tooltip label={tracker.string.AddSubIssues} props={{ subIssues: 1 }} direction="bottom">
<Button
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
size="small"
kind="transparent"
on:click={() => {
closeTooltip()
isCreating = true
}}
/>
</Tooltip> -->
</div>
{#if hasSubIssues}
<div class="mt-1">
{#if subIssues && issueStatuses}
<div class="list" class:collapsed={isCollapsed}>
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
<SubIssueList issues={subIssues} {issueStatuses} on:move={handleIssueSwap} />
</ExpandCollapse>
</div>
{:else}
<div class="flex-center pt-3">
<Spinner />
</div>
{/if}
</div>
{/if}
<style lang="scss">
.list {
border-bottom: 1px solid var(--divider-color);
&.collapsed {
margin-top: 1px;
border-bottom: none;
}
}
</style>

View File

@ -91,7 +91,8 @@ export default mergeIds(trackerId, tracker, {
ChangeParent: '' as IntlString, ChangeParent: '' as IntlString,
RemoveParent: '' as IntlString, RemoveParent: '' as IntlString,
OpenParent: '' as IntlString, OpenParent: '' as IntlString,
OpenSub: '' as IntlString, OpenSubIssues: '' as IntlString,
AddSubIssues: '' as IntlString,
BlockedBy: '' as IntlString, BlockedBy: '' as IntlString,
RelatedTo: '' as IntlString, RelatedTo: '' as IntlString,
Comments: '' as IntlString, Comments: '' as IntlString,