mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +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>
|
||||
|
||||
<div class="flex-col kanban-container top-divider">
|
||||
<div class="scrollable">
|
||||
<ScrollBox>
|
||||
<div class="kanban-content">
|
||||
{#each states as state, si}
|
||||
{@const stateObjects = getStateObjects(objects, state, dragCard)}
|
||||
<div class="kanban-container top-divider">
|
||||
<ScrollBox>
|
||||
<div class="kanban-content">
|
||||
{#each states as state, si}
|
||||
{@const stateObjects = getStateObjects(objects, state, dragCard)}
|
||||
|
||||
<div
|
||||
class="panel-container step-lr75"
|
||||
bind:this={stateRefs[si]}
|
||||
on:dragover={(event) => panelDragOver(event, state)}
|
||||
on:drop={() => {
|
||||
move(state._id)
|
||||
isDragging = false
|
||||
}}
|
||||
>
|
||||
{#if $$slots.header !== undefined}
|
||||
<slot name="header" state={toAny(state)} count={stateObjects.length} />
|
||||
{:else}
|
||||
<div class="header">
|
||||
<div class="bar" style="background-color: {getPlatformColor(state.color)}" />
|
||||
<div class="flex-between label">
|
||||
<div>
|
||||
<span class="lines-limit-2">{state.title}</span>
|
||||
</div>
|
||||
<div
|
||||
class="panel-container step-lr75"
|
||||
bind:this={stateRefs[si]}
|
||||
on:dragover={(event) => panelDragOver(event, state)}
|
||||
on:drop={() => {
|
||||
move(state._id)
|
||||
isDragging = false
|
||||
}}
|
||||
>
|
||||
{#if $$slots.header !== undefined}
|
||||
<slot name="header" state={toAny(state)} count={stateObjects.length} />
|
||||
{:else}
|
||||
<div class="header">
|
||||
<div class="bar" style="background-color: {getPlatformColor(state.color)}" />
|
||||
<div class="flex-between label">
|
||||
<div>
|
||||
<span class="lines-limit-2">{state.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Scroller padding={'.5rem 0'} on:dragover on:drop>
|
||||
<slot name="beforeCard" {state} />
|
||||
{#each stateObjects as object}
|
||||
{@const dragged = isDragging && object.it._id === dragCard?._id}
|
||||
</div>
|
||||
{/if}
|
||||
<Scroller padding={'.5rem 0'} on:dragover on:drop>
|
||||
<slot name="beforeCard" {state} />
|
||||
{#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
|
||||
transition:slideD|local={{ isDragging }}
|
||||
class="step-tb75"
|
||||
on:dragover|preventDefault={(evt) => cardDragOver(evt, object)}
|
||||
on:drop|preventDefault={(evt) => cardDrop(evt, object)}
|
||||
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
|
||||
}}
|
||||
>
|
||||
<div
|
||||
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>
|
||||
<slot name="card" object={toAny(object.it)} {dragged} />
|
||||
</div>
|
||||
{/each}
|
||||
<slot name="afterCard" {space} {state} />
|
||||
</Scroller>
|
||||
</div>
|
||||
{/each}
|
||||
<slot name="afterPanel" />
|
||||
</div>
|
||||
</ScrollBox>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<slot name="afterCard" {space} {state} />
|
||||
</Scroller>
|
||||
</div>
|
||||
{/each}
|
||||
<slot name="afterPanel" />
|
||||
</div>
|
||||
</ScrollBox>
|
||||
{#if isDragging}
|
||||
<slot name="doneBar" onDone={updateDone} />
|
||||
{/if}
|
||||
@ -362,7 +360,6 @@
|
||||
.kanban-content {
|
||||
display: flex;
|
||||
padding: 1.5rem 2rem 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
@ -416,9 +413,7 @@
|
||||
.panel-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 20rem;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
@ -428,7 +423,6 @@
|
||||
flex-direction: column;
|
||||
height: 4rem;
|
||||
min-height: 4rem;
|
||||
user-select: none;
|
||||
|
||||
.bar {
|
||||
height: 0.375rem;
|
||||
|
@ -221,7 +221,7 @@ input.search {
|
||||
min-width: 0;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
color: var(--theme-content-accent-color);
|
||||
color: var(--accent-color);
|
||||
|
||||
overflow: hidden;
|
||||
visibility: visible;
|
||||
@ -259,6 +259,16 @@ input.search {
|
||||
align-items: baseline;
|
||||
.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 {
|
||||
display: grid;
|
||||
|
@ -298,7 +298,7 @@
|
||||
&.link-bordered {
|
||||
padding: 0 0.375rem;
|
||||
color: var(--accent-color);
|
||||
border-color: var(--button-border-color);
|
||||
border-color: var(--divider-color);
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
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 CheckBox } from './components/CheckBox.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 ScrollBox } from './components/ScrollBox.svelte'
|
||||
export { default as PopupMenu } from './components/PopupMenu.svelte'
|
||||
|
@ -32,6 +32,7 @@
|
||||
const prioritiesInfo = defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] }))
|
||||
|
||||
const handlePriorityEditorOpened = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
|
@ -78,6 +78,7 @@
|
||||
}
|
||||
|
||||
const handleProjectEditorOpened = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
|
@ -17,8 +17,8 @@
|
||||
import { Class, Doc, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||
import { Kanban, TypeState } from '@anticrm/kanban'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import { Button, Icon, IconAdd, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import type { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Button, Icon, IconAdd, showPopup, Tooltip, showPanel } from '@anticrm/ui'
|
||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@anticrm/view-resources'
|
||||
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
|
||||
import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
|
||||
@ -29,6 +29,7 @@
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import PriorityEditor from './PriorityEditor.svelte'
|
||||
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
||||
import SubIssuesSelector from './edit/SubIssuesSelector.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
@ -43,17 +44,19 @@
|
||||
currentTeam = res.shift()
|
||||
})
|
||||
|
||||
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||
let states: TypeState[] | undefined
|
||||
$: statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ attachedTo: currentSpace },
|
||||
(issueStatuses) => {
|
||||
states = issueStatuses.map((status) => ({
|
||||
(is) => {
|
||||
states = is.map((status) => ({
|
||||
_id: status._id,
|
||||
title: status.name,
|
||||
color: status.color ?? status.$lookup?.category?.color ?? 0,
|
||||
icon: status.$lookup?.category?.icon ?? undefined
|
||||
}))
|
||||
issueStatuses = is
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
@ -152,7 +155,12 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="card" let: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">
|
||||
<IssuePresenter value={object} {currentTeam} />
|
||||
<span class="fs-bold caption-color mt-1 lines-limit-2">
|
||||
@ -168,7 +176,10 @@
|
||||
isEditable={true}
|
||||
/>
|
||||
</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
|
||||
value={issue}
|
||||
isEditable={true}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Issue, IssuePriority } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
import { tooltip } from '@anticrm/ui'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import PrioritySelector from '../PrioritySelector.svelte'
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
{#if value}
|
||||
{#if isEditable}
|
||||
<Tooltip label={tracker.string.SetPriority} fill>
|
||||
<div class="clear-mins" use:tooltip={{ label: tracker.string.SetPriority }}>
|
||||
<PrioritySelector
|
||||
{kind}
|
||||
{size}
|
||||
@ -61,7 +61,7 @@
|
||||
priority={value.priority}
|
||||
onPriorityChange={handlePriorityChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{:else}
|
||||
<PrioritySelector {kind} {size} {width} {justify} {isEditable} {shouldShowLabel} priority={value.priority} />
|
||||
{/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 { Issue, Project } from '@anticrm/tracker'
|
||||
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 tracker from '../../plugin'
|
||||
import ProjectSelector from '../ProjectSelector.svelte'
|
||||
@ -52,7 +52,10 @@
|
||||
</script>
|
||||
|
||||
{#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
|
||||
{kind}
|
||||
{size}
|
||||
@ -65,5 +68,5 @@
|
||||
value={value.project}
|
||||
onProjectIdChange={handleProjectIdChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user