mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-30 02:37:46 +03:00
Tracker: add Sub-issues list (#1989)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
parent
b1506d1a65
commit
30a9186eab
45
packages/ui/src/components/ExpandCollapse.svelte
Normal file
45
packages/ui/src/components/ExpandCollapse.svelte
Normal 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>
|
@ -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'
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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": "",
|
||||||
|
@ -43,6 +43,9 @@
|
|||||||
"Low": "Низкий",
|
"Low": "Низкий",
|
||||||
"Unassigned": "Не назначен",
|
"Unassigned": "Не назначен",
|
||||||
"AddIssueTooltip": "Добавить задачу\u2026",
|
"AddIssueTooltip": "Добавить задачу\u2026",
|
||||||
|
"SubIssues": "Подзадачи ({subIssues})",
|
||||||
|
"OpenSubIssues": "Открыть подзадачи",
|
||||||
|
"AddSubIssues": "{subIssues, plural, =1 {Добавить подзадачу} other {+ Добавить подзадачи}}",
|
||||||
|
|
||||||
"Title": "Заголовок",
|
"Title": "Заголовок",
|
||||||
"Description": "",
|
"Description": "",
|
||||||
|
@ -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>
|
@ -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}
|
||||||
|
@ -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} />
|
||||||
|
|
||||||
|
@ -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>
|
@ -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"
|
||||||
|
@ -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>
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user