mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-22 16:27:22 +00:00
Tracker: fix issue status colors in the kanban view (#2231)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
parent
6def1db421
commit
1a90f99af5
@ -1,217 +0,0 @@
|
|||||||
<!--
|
|
||||||
// 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 contact from '@anticrm/contact'
|
|
||||||
import { Class, Doc, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
|
||||||
import { Kanban, TypeState } from '@anticrm/kanban'
|
|
||||||
import { createQuery } from '@anticrm/presentation'
|
|
||||||
import type { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
|
||||||
import { Button, Icon, IconAdd, showPopup, showPanel, Component, getPlatformColor } 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'
|
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import tracker from '../../plugin'
|
|
||||||
import notification from '@anticrm/notification'
|
|
||||||
import CreateIssue from '../CreateIssue.svelte'
|
|
||||||
import AssigneePresenter from './AssigneePresenter.svelte'
|
|
||||||
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
|
|
||||||
|
|
||||||
const spaceQuery = createQuery()
|
|
||||||
const statusesQuery = createQuery()
|
|
||||||
|
|
||||||
let currentTeam: Team | undefined
|
|
||||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
|
||||||
currentTeam = res.shift()
|
|
||||||
})
|
|
||||||
|
|
||||||
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
|
||||||
let states: TypeState[] | undefined
|
|
||||||
$: statusesQuery.query(
|
|
||||||
tracker.class.IssueStatus,
|
|
||||||
{ attachedTo: currentSpace },
|
|
||||||
(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 },
|
|
||||||
sort: { rank: SortingOrder.Ascending }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function toIssue (object: any): WithLookup<Issue> {
|
|
||||||
return object as WithLookup<Issue>
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: FindOptions<Issue> = {
|
|
||||||
lookup: {
|
|
||||||
assignee: contact.class.Employee,
|
|
||||||
space: tracker.class.Team,
|
|
||||||
_id: {
|
|
||||||
subIssues: tracker.class.Issue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let kanbanUI: Kanban
|
|
||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
|
||||||
kanbanUI.select(offset, of, dir)
|
|
||||||
})
|
|
||||||
onMount(() => {
|
|
||||||
;(document.activeElement as HTMLElement)?.blur()
|
|
||||||
})
|
|
||||||
|
|
||||||
const showMenu = async (ev: MouseEvent, items: Doc[]): Promise<void> => {
|
|
||||||
ev.preventDefault()
|
|
||||||
showPopup(
|
|
||||||
Menu,
|
|
||||||
{ object: items, baseMenuClass },
|
|
||||||
{
|
|
||||||
getBoundingClientRect: () => DOMRect.fromRect({ width: 1, height: 1, x: ev.clientX, y: ev.clientY })
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// selection = undefined
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if currentTeam && states}
|
|
||||||
<ActionContext
|
|
||||||
context={{
|
|
||||||
mode: 'browser'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div class="flex-between label font-medium w-full p-4">Board</div>
|
|
||||||
<Kanban
|
|
||||||
bind:this={kanbanUI}
|
|
||||||
_class={tracker.class.Issue}
|
|
||||||
search=""
|
|
||||||
{states}
|
|
||||||
{options}
|
|
||||||
query={{ attachedTo: tracker.ids.NoParent }}
|
|
||||||
fieldName={'status'}
|
|
||||||
rankFieldName={'rank'}
|
|
||||||
on:content={(evt) => {
|
|
||||||
listProvider.update(evt.detail)
|
|
||||||
}}
|
|
||||||
on:obj-focus={(evt) => {
|
|
||||||
listProvider.updateFocus(evt.detail)
|
|
||||||
}}
|
|
||||||
selection={listProvider.current($focusStore)}
|
|
||||||
checked={$selectionStore ?? []}
|
|
||||||
on:check={(evt) => {
|
|
||||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
|
||||||
}}
|
|
||||||
on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="header" let:state let:count>
|
|
||||||
<div class="header flex-col">
|
|
||||||
<div class="flex-between label font-medium w-full h-full">
|
|
||||||
<div class="flex-row-center gap-2">
|
|
||||||
<Icon icon={state.icon} fill={getPlatformColor(state.color)} size={'small'} />
|
|
||||||
<span class="lines-limit-2 ml-2">{state.title}</span>
|
|
||||||
<span class="counter ml-2 text-md">{count}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<Button
|
|
||||||
icon={IconAdd}
|
|
||||||
kind={'transparent'}
|
|
||||||
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
|
|
||||||
on:click={() => {
|
|
||||||
showPopup(CreateIssue, { space: currentSpace, status: state._id }, 'top')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</svelte:fragment>
|
|
||||||
<svelte:fragment slot="card" let:object>
|
|
||||||
{@const issue = toIssue(object)}
|
|
||||||
<div
|
|
||||||
class="tracker-card"
|
|
||||||
on:click={() => {
|
|
||||||
showPanel(tracker.component.EditIssue, object._id, object._class, 'content')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex-col mr-6">
|
|
||||||
<IssuePresenter value={object} />
|
|
||||||
<span class="fs-bold caption-color mt-1 lines-limit-2">
|
|
||||||
{object.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="abs-rt-content">
|
|
||||||
<AssigneePresenter
|
|
||||||
value={issue.$lookup?.assignee}
|
|
||||||
defaultClass={contact.class.Employee}
|
|
||||||
issueId={issue._id}
|
|
||||||
isEditable={true}
|
|
||||||
/>
|
|
||||||
<div class="flex-center mt-2">
|
|
||||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="buttons-group xxsmall-gap mt-10px">
|
|
||||||
{#if issue && issueStatuses && issue.subIssues > 0}
|
|
||||||
<SubIssuesSelector {issue} {currentTeam} {issueStatuses} />
|
|
||||||
{/if}
|
|
||||||
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
|
|
||||||
<ProjectEditor
|
|
||||||
value={issue}
|
|
||||||
isEditable={true}
|
|
||||||
kind={'link-bordered'}
|
|
||||||
size={'inline'}
|
|
||||||
justify={'center'}
|
|
||||||
width={''}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</svelte:fragment>
|
|
||||||
</Kanban>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.header {
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: var(--caption-color);
|
|
||||||
.counter {
|
|
||||||
color: rgba(var(--caption-color), 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tracker-card {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
min-height: 6.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -41,9 +41,10 @@
|
|||||||
$: color =
|
$: color =
|
||||||
fill ??
|
fill ??
|
||||||
(value.color !== undefined ? getPlatformColor(value.color) : undefined) ??
|
(value.color !== undefined ? getPlatformColor(value.color) : undefined) ??
|
||||||
(category !== undefined ? getPlatformColor(category.color) : undefined)
|
(category !== undefined ? getPlatformColor(category.color) : undefined) ??
|
||||||
|
'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if icon !== undefined && color !== undefined}
|
{#if icon !== undefined}
|
||||||
<Icon {icon} fill={color} {size} />
|
<Icon {icon} fill={color} {size} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -26,9 +26,9 @@
|
|||||||
IconAdd,
|
IconAdd,
|
||||||
showPanel,
|
showPanel,
|
||||||
showPopup,
|
showPopup,
|
||||||
getPlatformColor,
|
|
||||||
Loading,
|
Loading,
|
||||||
tooltip
|
tooltip,
|
||||||
|
getPlatformColor
|
||||||
} from '@anticrm/ui'
|
} 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'
|
||||||
@ -40,7 +40,8 @@
|
|||||||
getKanbanStatuses,
|
getKanbanStatuses,
|
||||||
getPriorityStates,
|
getPriorityStates,
|
||||||
issuesGroupBySorting,
|
issuesGroupBySorting,
|
||||||
issuesSortOrderMap
|
issuesSortOrderMap,
|
||||||
|
UNSET_COLOR
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import CreateIssue from '../CreateIssue.svelte'
|
import CreateIssue from '../CreateIssue.svelte'
|
||||||
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
||||||
@ -195,7 +196,13 @@
|
|||||||
<div class="header flex-col">
|
<div class="header flex-col">
|
||||||
<div class="flex-between label font-medium w-full h-full">
|
<div class="flex-between label font-medium w-full h-full">
|
||||||
<div class="flex-row-center gap-2">
|
<div class="flex-row-center gap-2">
|
||||||
<Icon icon={state.icon} fill={getPlatformColor(state.color)} size={'small'} />
|
{#if state.icon}
|
||||||
|
<Icon
|
||||||
|
icon={state.icon}
|
||||||
|
fill={state.color === UNSET_COLOR ? 'currentColor' : getPlatformColor(state.color)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<span class="lines-limit-2 ml-2">{state.title}</span>
|
<span class="lines-limit-2 ml-2">{state.title}</span>
|
||||||
<span class="counter ml-2 text-md">{count}</span>
|
<span class="counter ml-2 text-md">{count}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +32,8 @@ import { defaultPriorities, defaultProjectStatuses, issuePriorities } from './ty
|
|||||||
|
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
|
export const UNSET_COLOR = -1
|
||||||
|
|
||||||
export interface NavigationItem {
|
export interface NavigationItem {
|
||||||
id: string
|
id: string
|
||||||
label: IntlString
|
label: IntlString
|
||||||
@ -344,7 +346,7 @@ export async function getKanbanStatuses (
|
|||||||
issues: Array<WithLookup<Issue>>
|
issues: Array<WithLookup<Issue>>
|
||||||
): Promise<TypeState[]> {
|
): Promise<TypeState[]> {
|
||||||
if (groupBy === IssuesGrouping.NoGrouping) {
|
if (groupBy === IssuesGrouping.NoGrouping) {
|
||||||
return [{ _id: undefined, color: 0, title: await translate(tracker.string.NoGrouping, {}) }]
|
return [{ _id: undefined, color: UNSET_COLOR, title: await translate(tracker.string.NoGrouping, {}) }]
|
||||||
}
|
}
|
||||||
if (groupBy === IssuesGrouping.Priority) {
|
if (groupBy === IssuesGrouping.Priority) {
|
||||||
const states = issues.reduce<TypeState[]>((result, issue) => {
|
const states = issues.reduce<TypeState[]>((result, issue) => {
|
||||||
@ -355,7 +357,7 @@ export async function getKanbanStatuses (
|
|||||||
{
|
{
|
||||||
_id: priority,
|
_id: priority,
|
||||||
title: issuePriorities[priority].label,
|
title: issuePriorities[priority].label,
|
||||||
color: 0,
|
color: UNSET_COLOR,
|
||||||
icon: issuePriorities[priority].icon
|
icon: issuePriorities[priority].icon
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -371,14 +373,14 @@ export async function getKanbanStatuses (
|
|||||||
return issues.reduce<TypeState[]>((result, issue) => {
|
return issues.reduce<TypeState[]>((result, issue) => {
|
||||||
const status = issue.$lookup?.status
|
const status = issue.$lookup?.status
|
||||||
if (status === undefined || result.find(({ _id }) => _id === status._id) !== undefined) return result
|
if (status === undefined || result.find(({ _id }) => _id === status._id) !== undefined) return result
|
||||||
const icon = '$lookup' in status ? status.$lookup?.category?.icon : undefined
|
const category = '$lookup' in status ? status.$lookup?.category : undefined
|
||||||
return [
|
return [
|
||||||
...result,
|
...result,
|
||||||
{
|
{
|
||||||
_id: status._id,
|
_id: status._id,
|
||||||
title: status.name,
|
title: status.name,
|
||||||
color: status.color ?? 0,
|
icon: category?.icon,
|
||||||
icon
|
color: status.color ?? category?.color ?? UNSET_COLOR
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}, [])
|
}, [])
|
||||||
@ -392,7 +394,7 @@ export async function getKanbanStatuses (
|
|||||||
{
|
{
|
||||||
_id: issue.assignee,
|
_id: issue.assignee,
|
||||||
title: issue.$lookup?.assignee?.name ?? noAssignee,
|
title: issue.$lookup?.assignee?.name ?? noAssignee,
|
||||||
color: 0,
|
color: UNSET_COLOR,
|
||||||
icon: undefined
|
icon: undefined
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -407,7 +409,7 @@ export async function getKanbanStatuses (
|
|||||||
{
|
{
|
||||||
_id: issue.project,
|
_id: issue.project,
|
||||||
title: issue.$lookup?.project?.label ?? noProject,
|
title: issue.$lookup?.project?.label ?? noProject,
|
||||||
color: 0,
|
color: UNSET_COLOR,
|
||||||
icon: undefined
|
icon: undefined
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -420,7 +422,7 @@ export function getIssueStatusStates (issueStatuses: Array<WithLookup<IssueStatu
|
|||||||
return issueStatuses.map((status) => ({
|
return issueStatuses.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 ?? UNSET_COLOR,
|
||||||
icon: status.$lookup?.category?.icon ?? undefined
|
icon: status.$lookup?.category?.icon ?? undefined
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -430,7 +432,7 @@ export async function getPriorityStates (): Promise<TypeState[]> {
|
|||||||
defaultPriorities.map(async (priority) => ({
|
defaultPriorities.map(async (priority) => ({
|
||||||
_id: priority,
|
_id: priority,
|
||||||
title: await translate(issuePriorities[priority].label, {}),
|
title: await translate(issuePriorities[priority].label, {}),
|
||||||
color: 0,
|
color: UNSET_COLOR,
|
||||||
icon: issuePriorities[priority].icon
|
icon: issuePriorities[priority].icon
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user