mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-26 13:47:26 +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 ListView } from './components/ListView.svelte'
|
||||
export { default as ToggleButton } from './components/ToggleButton.svelte'
|
||||
export { default as ExpandCollapse } from './components/ExpandCollapse.svelte'
|
||||
|
||||
export * from './types'
|
||||
export * from './location'
|
||||
|
@ -16,6 +16,7 @@
|
||||
// import type { Metadata } from '@anticrm/platform'
|
||||
import type { Metadata } from '@anticrm/platform'
|
||||
import { setMetadata } from '@anticrm/platform'
|
||||
import { Writable, writable } from 'svelte/store'
|
||||
|
||||
export function setMetadataLocalStorage (id: Metadata<string>, value: string | null): void {
|
||||
if (value != null) {
|
||||
@ -33,3 +34,14 @@ export function fetchMetadataLocalStorage (id: Metadata<string>): string | null
|
||||
}
|
||||
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",
|
||||
"RemoveParent": "Remove 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": "",
|
||||
"RelatedTo": "",
|
||||
"Comments": "",
|
||||
|
@ -43,6 +43,9 @@
|
||||
"Low": "Низкий",
|
||||
"Unassigned": "Не назначен",
|
||||
"AddIssueTooltip": "Добавить задачу\u2026",
|
||||
"SubIssues": "Подзадачи ({subIssues})",
|
||||
"OpenSubIssues": "Открыть подзадачи",
|
||||
"AddSubIssues": "{subIssues, plural, =1 {Добавить подзадачу} other {+ Добавить подзадачи}}",
|
||||
|
||||
"Title": "Заголовок",
|
||||
"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 { Issue, IssueStatus } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
import { Tooltip, TooltipAlignment } from '@anticrm/ui'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import StatusSelector from '../StatusSelector.svelte'
|
||||
@ -25,6 +25,7 @@
|
||||
export let statuses: WithLookup<IssueStatus>[]
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
export let tooltipAlignment: TooltipAlignment | undefined = undefined
|
||||
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
@ -52,7 +53,7 @@
|
||||
|
||||
{#if value}
|
||||
{#if isEditable}
|
||||
<Tooltip label={tracker.string.SetStatus} fill>
|
||||
<Tooltip label={tracker.string.SetStatus} direction={tooltipAlignment} fill>
|
||||
<StatusSelector
|
||||
{kind}
|
||||
{size}
|
||||
|
@ -38,6 +38,7 @@
|
||||
import ControlPanel from './ControlPanel.svelte'
|
||||
import CopyToClipboard from './CopyToClipboard.svelte'
|
||||
import SubIssueSelector from './SubIssueSelector.svelte'
|
||||
import SubIssues from './SubIssues.svelte'
|
||||
|
||||
export let _id: Ref<Issue>
|
||||
export let _class: Ref<Class<Issue>>
|
||||
@ -239,6 +240,9 @@
|
||||
<MessageViewer message={description} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<SubIssues {issue} {issueStatuses} />
|
||||
</div>
|
||||
{/if}
|
||||
<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" />
|
||||
</div>
|
||||
{:else}
|
||||
<Tooltip label={tracker.string.OpenSub} direction="bottom">
|
||||
<Tooltip label={tracker.string.OpenSubIssues} direction="bottom">
|
||||
<div
|
||||
bind:this={subIssuesElement}
|
||||
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,
|
||||
RemoveParent: '' as IntlString,
|
||||
OpenParent: '' as IntlString,
|
||||
OpenSub: '' as IntlString,
|
||||
OpenSubIssues: '' as IntlString,
|
||||
AddSubIssues: '' as IntlString,
|
||||
BlockedBy: '' as IntlString,
|
||||
RelatedTo: '' as IntlString,
|
||||
Comments: '' as IntlString,
|
||||
|
Loading…
Reference in New Issue
Block a user