mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 11:31:57 +03:00
Tracker: Updating cards in Kanban (#2032)
* Add ProgressCircle, SubIssuesSelector. Open card on click. Signed-off-by: Alexander Platov <sas_lord@mail.ru> * Fix format Signed-off-by: Alexander Platov <sas_lord@mail.ru> * Fix Signed-off-by: Alexander Platov <sas_lord@mail.ru> * Fix scrolls in Kanban Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
676aace675
commit
d234102402
@ -281,73 +281,71 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-col kanban-container top-divider">
|
<div class="kanban-container top-divider">
|
||||||
<div class="scrollable">
|
<ScrollBox>
|
||||||
<ScrollBox>
|
<div class="kanban-content">
|
||||||
<div class="kanban-content">
|
{#each states as state, si}
|
||||||
{#each states as state, si}
|
{@const stateObjects = getStateObjects(objects, state, dragCard)}
|
||||||
{@const stateObjects = getStateObjects(objects, state, dragCard)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="panel-container step-lr75"
|
class="panel-container step-lr75"
|
||||||
bind:this={stateRefs[si]}
|
bind:this={stateRefs[si]}
|
||||||
on:dragover={(event) => panelDragOver(event, state)}
|
on:dragover={(event) => panelDragOver(event, state)}
|
||||||
on:drop={() => {
|
on:drop={() => {
|
||||||
move(state._id)
|
move(state._id)
|
||||||
isDragging = false
|
isDragging = false
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if $$slots.header !== undefined}
|
{#if $$slots.header !== undefined}
|
||||||
<slot name="header" state={toAny(state)} count={stateObjects.length} />
|
<slot name="header" state={toAny(state)} count={stateObjects.length} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="bar" style="background-color: {getPlatformColor(state.color)}" />
|
<div class="bar" style="background-color: {getPlatformColor(state.color)}" />
|
||||||
<div class="flex-between label">
|
<div class="flex-between label">
|
||||||
<div>
|
<div>
|
||||||
<span class="lines-limit-2">{state.title}</span>
|
<span class="lines-limit-2">{state.title}</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
<Scroller padding={'.5rem 0'} on:dragover on:drop>
|
{/if}
|
||||||
<slot name="beforeCard" {state} />
|
<Scroller padding={'.5rem 0'} on:dragover on:drop>
|
||||||
{#each stateObjects as object}
|
<slot name="beforeCard" {state} />
|
||||||
{@const dragged = isDragging && object.it._id === dragCard?._id}
|
{#each stateObjects as object}
|
||||||
|
{@const dragged = isDragging && object.it._id === dragCard?._id}
|
||||||
|
<div
|
||||||
|
transition:slideD|local={{ isDragging }}
|
||||||
|
class="step-tb75"
|
||||||
|
on:dragover|preventDefault={(evt) => cardDragOver(evt, object)}
|
||||||
|
on:drop|preventDefault={(evt) => cardDrop(evt, object)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
transition:slideD|local={{ isDragging }}
|
class="card-container"
|
||||||
class="step-tb75"
|
class:selection={selection !== undefined ? objects[selection]?._id === object.it._id : false}
|
||||||
on:dragover|preventDefault={(evt) => cardDragOver(evt, object)}
|
class:checked={checkedSet.has(object.it._id)}
|
||||||
on:drop|preventDefault={(evt) => cardDrop(evt, object)}
|
on:mouseover={() => dispatch('obj-focus', object.it)}
|
||||||
|
on:focus={() => {}}
|
||||||
|
on:contextmenu={(evt) => showMenu(evt, object)}
|
||||||
|
draggable={true}
|
||||||
|
class:draggable={true}
|
||||||
|
on:dragstart
|
||||||
|
on:dragend
|
||||||
|
class:dragged
|
||||||
|
on:dragstart={() => onDragStart(object, state)}
|
||||||
|
on:dragend={() => {
|
||||||
|
isDragging = false
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<slot name="card" object={toAny(object.it)} {dragged} />
|
||||||
class="card-container"
|
|
||||||
class:selection={selection !== undefined ? objects[selection]?._id === object.it._id : false}
|
|
||||||
class:checked={checkedSet.has(object.it._id)}
|
|
||||||
on:mouseover={() => dispatch('obj-focus', object.it)}
|
|
||||||
on:focus={() => {}}
|
|
||||||
on:contextmenu={(evt) => showMenu(evt, object)}
|
|
||||||
draggable={true}
|
|
||||||
class:draggable={true}
|
|
||||||
on:dragstart
|
|
||||||
on:dragend
|
|
||||||
class:dragged
|
|
||||||
on:dragstart={() => onDragStart(object, state)}
|
|
||||||
on:dragend={() => {
|
|
||||||
isDragging = false
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<slot name="card" object={toAny(object.it)} {dragged} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
</div>
|
||||||
<slot name="afterCard" {space} {state} />
|
{/each}
|
||||||
</Scroller>
|
<slot name="afterCard" {space} {state} />
|
||||||
</div>
|
</Scroller>
|
||||||
{/each}
|
</div>
|
||||||
<slot name="afterPanel" />
|
{/each}
|
||||||
</div>
|
<slot name="afterPanel" />
|
||||||
</ScrollBox>
|
</div>
|
||||||
</div>
|
</ScrollBox>
|
||||||
{#if isDragging}
|
{#if isDragging}
|
||||||
<slot name="doneBar" onDone={updateDone} />
|
<slot name="doneBar" onDone={updateDone} />
|
||||||
{/if}
|
{/if}
|
||||||
@ -362,7 +360,6 @@
|
|||||||
.kanban-content {
|
.kanban-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1.5rem 2rem 0;
|
padding: 1.5rem 2rem 0;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
@ -416,9 +413,7 @@
|
|||||||
.panel-container {
|
.panel-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
height: 100%;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
@ -428,7 +423,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
min-height: 4rem;
|
min-height: 4rem;
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
height: 0.375rem;
|
height: 0.375rem;
|
||||||
|
@ -221,7 +221,7 @@ input.search {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--theme-content-accent-color);
|
color: var(--accent-color);
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
@ -259,6 +259,16 @@ input.search {
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
.icon { transform: translateY(.2rem); }
|
.icon { transform: translateY(.2rem); }
|
||||||
}
|
}
|
||||||
|
// Presenters on the card
|
||||||
|
.card-container .flex-presenter,
|
||||||
|
.card-container .inline-presenter {
|
||||||
|
.icon { display: none; }
|
||||||
|
.label {
|
||||||
|
font-size: .75rem;
|
||||||
|
color: var(--dark-color);
|
||||||
|
}
|
||||||
|
&:hover .label { color: var(--content-color); }
|
||||||
|
}
|
||||||
|
|
||||||
.buttons-group {
|
.buttons-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -298,7 +298,7 @@
|
|||||||
&.link-bordered {
|
&.link-bordered {
|
||||||
padding: 0 0.375rem;
|
padding: 0 0.375rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
border-color: var(--button-border-color);
|
border-color: var(--divider-color);
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
background-color: var(--button-bg-hover);
|
background-color: var(--button-bg-hover);
|
||||||
|
67
packages/ui/src/components/ProgressCircle.svelte
Normal file
67
packages/ui/src/components/ProgressCircle.svelte
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2020 Anticrm Platform Contributors.
|
||||||
|
//
|
||||||
|
// 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 { getPlatformColor } from '../colors'
|
||||||
|
import { IconSize } from '../types'
|
||||||
|
|
||||||
|
export let value: number
|
||||||
|
export let min: number = 0
|
||||||
|
export let max: number = 100
|
||||||
|
export let color: number = 5
|
||||||
|
export let size: IconSize = 'small'
|
||||||
|
export let primary: boolean = false
|
||||||
|
|
||||||
|
if (value > max) value = max
|
||||||
|
if (value < min) value = min
|
||||||
|
|
||||||
|
const lenghtC: number = Math.PI * 14 - 1
|
||||||
|
const procC: number = lenghtC / (max - min)
|
||||||
|
$: dashOffset = (value - min) * procC
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg class="svg-{size}" fill="none" viewBox="0 0 16 16">
|
||||||
|
<circle
|
||||||
|
cx={8}
|
||||||
|
cy={8}
|
||||||
|
r={7}
|
||||||
|
class="progress-circle"
|
||||||
|
style:stroke={'var(--divider-color)'}
|
||||||
|
style:opacity={'.5'}
|
||||||
|
style:transform={`rotate(${-78 + ((dashOffset + 1) * 360) / (lenghtC + 1)}deg)`}
|
||||||
|
style:stroke-dasharray={lenghtC}
|
||||||
|
style:stroke-dashoffset={dashOffset === 0 ? 0 : dashOffset + 3}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={8}
|
||||||
|
cy={8}
|
||||||
|
r={7}
|
||||||
|
class="progress-circle"
|
||||||
|
style:stroke={primary ? 'var(--primary-bg-color)' : getPlatformColor(color)}
|
||||||
|
style:opacity={dashOffset === 0 ? 0 : 1}
|
||||||
|
style:transform={'rotate(-82deg)'}
|
||||||
|
style:stroke-dasharray={lenghtC}
|
||||||
|
style:stroke-dashoffset={dashOffset === 0 ? lenghtC : lenghtC - dashOffset + 1}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.progress-circle {
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transform-origin: center;
|
||||||
|
transition: transform 0.6s ease 0s, stroke-dashoffset 0.6s ease 0s, stroke-dasharray 0.6s ease 0s,
|
||||||
|
opacity 0.6s ease 0s;
|
||||||
|
}
|
||||||
|
</style>
|
@ -54,6 +54,7 @@ export { default as Tooltip } from './components/Tooltip.svelte'
|
|||||||
export { default as TooltipInstance } from './components/TooltipInstance.svelte'
|
export { default as TooltipInstance } from './components/TooltipInstance.svelte'
|
||||||
export { default as CheckBox } from './components/CheckBox.svelte'
|
export { default as CheckBox } from './components/CheckBox.svelte'
|
||||||
export { default as Progress } from './components/Progress.svelte'
|
export { default as Progress } from './components/Progress.svelte'
|
||||||
|
export { default as ProgressCircle } from './components/ProgressCircle.svelte'
|
||||||
export { default as Tabs } from './components/Tabs.svelte'
|
export { default as Tabs } from './components/Tabs.svelte'
|
||||||
export { default as ScrollBox } from './components/ScrollBox.svelte'
|
export { default as ScrollBox } from './components/ScrollBox.svelte'
|
||||||
export { default as PopupMenu } from './components/PopupMenu.svelte'
|
export { default as PopupMenu } from './components/PopupMenu.svelte'
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
const prioritiesInfo = defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] }))
|
const prioritiesInfo = defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] }))
|
||||||
|
|
||||||
const handlePriorityEditorOpened = (event: MouseEvent) => {
|
const handlePriorityEditorOpened = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
if (!isEditable) {
|
if (!isEditable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleProjectEditorOpened = (event: MouseEvent) => {
|
const handleProjectEditorOpened = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
if (!isEditable) {
|
if (!isEditable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
import { Class, Doc, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
import { Class, Doc, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||||
import { Kanban, TypeState } from '@anticrm/kanban'
|
import { Kanban, TypeState } from '@anticrm/kanban'
|
||||||
import { createQuery } from '@anticrm/presentation'
|
import { createQuery } from '@anticrm/presentation'
|
||||||
import { Issue, Team } from '@anticrm/tracker'
|
import type { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||||
import { Button, Icon, IconAdd, showPopup, Tooltip } from '@anticrm/ui'
|
import { Button, Icon, IconAdd, showPopup, Tooltip, showPanel } from '@anticrm/ui'
|
||||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@anticrm/view-resources'
|
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@anticrm/view-resources'
|
||||||
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
||||||
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
||||||
@ -29,6 +29,7 @@
|
|||||||
import IssuePresenter from './IssuePresenter.svelte'
|
import IssuePresenter from './IssuePresenter.svelte'
|
||||||
import PriorityEditor from './PriorityEditor.svelte'
|
import PriorityEditor from './PriorityEditor.svelte'
|
||||||
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
||||||
|
import SubIssuesSelector from './edit/SubIssuesSelector.svelte'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||||
@ -43,17 +44,19 @@
|
|||||||
currentTeam = res.shift()
|
currentTeam = res.shift()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||||
let states: TypeState[] | undefined
|
let states: TypeState[] | undefined
|
||||||
$: statusesQuery.query(
|
$: statusesQuery.query(
|
||||||
tracker.class.IssueStatus,
|
tracker.class.IssueStatus,
|
||||||
{ attachedTo: currentSpace },
|
{ attachedTo: currentSpace },
|
||||||
(issueStatuses) => {
|
(is) => {
|
||||||
states = issueStatuses.map((status) => ({
|
states = is.map((status) => ({
|
||||||
_id: status._id,
|
_id: status._id,
|
||||||
title: status.name,
|
title: status.name,
|
||||||
color: status.color ?? status.$lookup?.category?.color ?? 0,
|
color: status.color ?? status.$lookup?.category?.color ?? 0,
|
||||||
icon: status.$lookup?.category?.icon ?? undefined
|
icon: status.$lookup?.category?.icon ?? undefined
|
||||||
}))
|
}))
|
||||||
|
issueStatuses = is
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
lookup: { category: tracker.class.IssueStatusCategory },
|
lookup: { category: tracker.class.IssueStatusCategory },
|
||||||
@ -152,7 +155,12 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="card" let:object>
|
<svelte:fragment slot="card" let:object>
|
||||||
{@const issue = toIssue(object)}
|
{@const issue = toIssue(object)}
|
||||||
<div class="tracker-card">
|
<div
|
||||||
|
class="tracker-card"
|
||||||
|
on:click={() => {
|
||||||
|
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="flex-col mr-6">
|
<div class="flex-col mr-6">
|
||||||
<IssuePresenter value={object} {currentTeam} />
|
<IssuePresenter value={object} {currentTeam} />
|
||||||
<span class="fs-bold caption-color mt-1 lines-limit-2">
|
<span class="fs-bold caption-color mt-1 lines-limit-2">
|
||||||
@ -168,7 +176,10 @@
|
|||||||
isEditable={true}
|
isEditable={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons-group xsmall-gap mt-10px">
|
<div class="buttons-group xxsmall-gap mt-10px">
|
||||||
|
{#if issue && issueStatuses && issue.subIssues > 0}
|
||||||
|
<SubIssuesSelector {issue} {currentTeam} {issueStatuses} />
|
||||||
|
{/if}
|
||||||
<PriorityEditor
|
<PriorityEditor
|
||||||
value={issue}
|
value={issue}
|
||||||
isEditable={true}
|
isEditable={true}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Issue, IssuePriority } from '@anticrm/tracker'
|
import { Issue, IssuePriority } from '@anticrm/tracker'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import { Tooltip } from '@anticrm/ui'
|
import { tooltip } 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 PrioritySelector from '../PrioritySelector.svelte'
|
import PrioritySelector from '../PrioritySelector.svelte'
|
||||||
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
{#if value}
|
{#if value}
|
||||||
{#if isEditable}
|
{#if isEditable}
|
||||||
<Tooltip label={tracker.string.SetPriority} fill>
|
<div class="clear-mins" use:tooltip={{ label: tracker.string.SetPriority }}>
|
||||||
<PrioritySelector
|
<PrioritySelector
|
||||||
{kind}
|
{kind}
|
||||||
{size}
|
{size}
|
||||||
@ -61,7 +61,7 @@
|
|||||||
priority={value.priority}
|
priority={value.priority}
|
||||||
onPriorityChange={handlePriorityChanged}
|
onPriorityChange={handlePriorityChanged}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<PrioritySelector {kind} {size} {width} {justify} {isEditable} {shouldShowLabel} priority={value.priority} />
|
<PrioritySelector {kind} {size} {width} {justify} {isEditable} {shouldShowLabel} priority={value.priority} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
<!--
|
||||||
|
// 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, Ref, Doc } from '@anticrm/core'
|
||||||
|
import { createQuery } from '@anticrm/presentation'
|
||||||
|
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||||
|
import { Button, ProgressCircle, showPopup, SelectPopup, closeTooltip, showPanel } from '@anticrm/ui'
|
||||||
|
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||||
|
import tracker from '../../../plugin'
|
||||||
|
|
||||||
|
export let issue: Issue
|
||||||
|
export let currentTeam: Team | undefined
|
||||||
|
export let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||||
|
|
||||||
|
export let kind: ButtonKind = 'link-bordered'
|
||||||
|
export let size: ButtonSize = 'inline'
|
||||||
|
export let justify: 'left' | 'center' = 'left'
|
||||||
|
export let width: string | undefined = 'min-contet'
|
||||||
|
|
||||||
|
const subIssuesQuery = createQuery()
|
||||||
|
let btn: HTMLElement
|
||||||
|
|
||||||
|
let subIssues: Issue[] | undefined
|
||||||
|
let doneStatus: Ref<Doc> | undefined
|
||||||
|
let countComplate: number = 0
|
||||||
|
|
||||||
|
$: hasSubIssues = issue.subIssues > 0
|
||||||
|
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
|
||||||
|
sort: { rank: SortingOrder.Ascending }
|
||||||
|
})
|
||||||
|
$: if (issueStatuses && subIssues) {
|
||||||
|
doneStatus = issueStatuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined
|
||||||
|
if (doneStatus) countComplate = subIssues.filter((si) => si.status === doneStatus).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIssueStatusIcon (issue: Issue) {
|
||||||
|
return issueStatuses?.find((s) => issue.status === s._id)?.$lookup?.category?.icon ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIssueId (issue: Issue) {
|
||||||
|
return `${currentTeam?.identifier}-${issue.number}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function openIssue (target: Ref<Issue>) {
|
||||||
|
if (target !== issue._id) {
|
||||||
|
showPanel(tracker.component.EditIssue, target, issue._class, 'content')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSubIssues () {
|
||||||
|
if (subIssues) {
|
||||||
|
closeTooltip()
|
||||||
|
showPopup(
|
||||||
|
SelectPopup,
|
||||||
|
{
|
||||||
|
value: subIssues.map((iss) => ({
|
||||||
|
id: iss._id,
|
||||||
|
icon: getIssueStatusIcon(iss),
|
||||||
|
text: `${getIssueId(iss)} ${iss.title}`,
|
||||||
|
isSelected: iss._id === issue._id
|
||||||
|
})),
|
||||||
|
width: 'large'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getBoundingClientRect: () => {
|
||||||
|
const rect = btn.getBoundingClientRect()
|
||||||
|
const offsetX = 0
|
||||||
|
const offsetY = 0
|
||||||
|
|
||||||
|
return DOMRect.fromRect({ width: 1, height: 1, x: rect.left + offsetX, y: rect.bottom + offsetY })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(selectedIssue) => selectedIssue !== undefined && openIssue(selectedIssue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if hasSubIssues}
|
||||||
|
<div class="flex-center clear-mins" bind:this={btn}>
|
||||||
|
<Button
|
||||||
|
{width}
|
||||||
|
{kind}
|
||||||
|
{size}
|
||||||
|
{justify}
|
||||||
|
on:click={(ev) => {
|
||||||
|
ev.stopPropagation()
|
||||||
|
if (subIssues) showSubIssues()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="content">
|
||||||
|
{#if subIssues}
|
||||||
|
<div class="flex-row-center content-color text-sm">
|
||||||
|
<div class="mr-1">
|
||||||
|
<ProgressCircle bind:value={countComplate} bind:max={subIssues.length} size={'inline'} primary />
|
||||||
|
</div>
|
||||||
|
{countComplate}/{subIssues.length}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -16,7 +16,7 @@
|
|||||||
import { Ref } from '@anticrm/core'
|
import { Ref } from '@anticrm/core'
|
||||||
import { Issue, Project } from '@anticrm/tracker'
|
import { Issue, Project } from '@anticrm/tracker'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import { ButtonKind, ButtonShape, ButtonSize, Tooltip } from '@anticrm/ui'
|
import { ButtonKind, ButtonShape, ButtonSize, tooltip } from '@anticrm/ui'
|
||||||
import { IntlString } from '@anticrm/platform'
|
import { IntlString } from '@anticrm/platform'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
import ProjectSelector from '../ProjectSelector.svelte'
|
import ProjectSelector from '../ProjectSelector.svelte'
|
||||||
@ -52,7 +52,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value.project || shouldShowPlaceholder}
|
{#if value.project || shouldShowPlaceholder}
|
||||||
<Tooltip label={value.project ? tracker.string.MoveToProject : tracker.string.AddToProject} fill>
|
<div
|
||||||
|
class="clear-mins"
|
||||||
|
use:tooltip={{ label: value.project ? tracker.string.MoveToProject : tracker.string.AddToProject }}
|
||||||
|
>
|
||||||
<ProjectSelector
|
<ProjectSelector
|
||||||
{kind}
|
{kind}
|
||||||
{size}
|
{size}
|
||||||
@ -65,5 +68,5 @@
|
|||||||
value={value.project}
|
value={value.project}
|
||||||
onProjectIdChange={handleProjectIdChanged}
|
onProjectIdChange={handleProjectIdChanged}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
Loading…
Reference in New Issue
Block a user