mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-31 23:46:12 +03:00
Tracker: View options - Grouping (#1442)
Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
parent
c24a27c2ba
commit
e4e39469c6
9
packages/ui/src/components/icons/Options.svelte
Normal file
9
packages/ui/src/components/icons/Options.svelte
Normal file
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.34943 2.12195L4.41226 2.18673L6.84829 5.19443C6.9204 5.28346 6.95975 5.39455 6.95975 5.50912C6.95975 5.75458 6.78287 5.95873 6.54962 6.00107L6.45975 6.00912L5.2009 6.00836C5.08013 8.94612 4.75119 13.9988 4.01089 13.9988C3.42906 13.9988 3.03779 11.4583 2.83711 6.37732L2.8231 6.01007L1.50001 6.00912C1.38246 6.00912 1.26867 5.96771 1.17862 5.89215C0.990587 5.73437 0.947891 5.46765 1.06539 5.26176L1.11699 5.18773L3.64068 2.18003C3.66126 2.15551 3.68414 2.13302 3.70902 2.11288C3.89976 1.95839 4.16972 1.96734 4.34943 2.12195ZM11.2536 12.0016C11.6675 12.0016 12.003 12.3371 12.003 12.751C12.003 13.1649 11.6675 13.5004 11.2536 13.5004H8.75634C8.34245 13.5004 8.00693 13.1649 8.00693 12.751C8.00693 12.3371 8.34245 12.0016 8.75634 12.0016H11.2536ZM12.2526 9.00393C12.6665 9.00393 13.002 9.33946 13.002 9.75335C13.002 10.1672 12.6665 10.5028 12.2526 10.5028H8.75634C8.34245 10.5028 8.00693 10.1672 8.00693 9.75335C8.00693 9.33946 8.34245 9.00393 8.75634 9.00393H12.2526ZM13.2516 6.00629C13.6655 6.00629 14.001 6.34182 14.001 6.7557C14.001 7.16959 13.6655 7.50511 13.2516 7.50511H8.75634C8.34245 7.50511 8.00693 7.16959 8.00693 6.7557C8.00693 6.34182 8.34245 6.00629 8.75634 6.00629H13.2516ZM14.2506 3.00865C14.6645 3.00865 15 3.34418 15 3.75806C15 4.17195 14.6645 4.50747 14.2506 4.50747H8.75634C8.34245 4.50747 8.00693 4.17195 8.00693 3.75806C8.00693 3.34418 8.34245 3.00865 8.75634 3.00865H14.2506Z"/>
|
||||
</svg>
|
||||
|
@ -104,6 +104,7 @@ export { default as IconNavPrev } from './components/icons/NavPrev.svelte'
|
||||
export { default as IconNavNext } from './components/icons/NavNext.svelte'
|
||||
export { default as IconDPCalendar } from './components/calendar/icons/DPCalendar.svelte'
|
||||
export { default as IconDPCalendarOver } from './components/calendar/icons/DPCalendarOver.svelte'
|
||||
export { default as IconOptions } from './components/icons/Options.svelte'
|
||||
|
||||
export { default as PanelInstance } from './components/PanelInstance.svelte'
|
||||
export { default as Panel } from './components/Panel.svelte'
|
||||
|
@ -38,7 +38,7 @@
|
||||
|
||||
"Title": "Title",
|
||||
"Description": "",
|
||||
"Status": "",
|
||||
"Status": "Status",
|
||||
"Number": "Number",
|
||||
"Assignee": "Assignee",
|
||||
"AssignTo": "Assign to...",
|
||||
@ -51,7 +51,7 @@
|
||||
"Labels": "Labels",
|
||||
"Project": "Project",
|
||||
"Space": "",
|
||||
"DueDate": "Set due date\u2026",
|
||||
"SetDueDate": "Set due date\u2026",
|
||||
"ModificationDate": "Updated {value}",
|
||||
"Team": "",
|
||||
"Issue": "",
|
||||
@ -65,7 +65,13 @@
|
||||
"DueDatePopupTitle": "Due on {value}",
|
||||
"DueDatePopupOverdueTitle": "Was due on {value}",
|
||||
"DueDatePopupDescription": "{value, plural, =0 {Today} =1 {Tomorrow} other {# days remaining}}",
|
||||
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 day overdue} other {# days overdue}}"
|
||||
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 day overdue} other {# days overdue}}",
|
||||
"Grouping": "Grouping",
|
||||
"Ordering": "Ordering",
|
||||
"NoGrouping": "No grouping",
|
||||
"NoAssignee": "No assignee",
|
||||
"LastUpdated": "Last updated",
|
||||
"DueDate": "Due date"
|
||||
},
|
||||
"status": {}
|
||||
}
|
@ -28,11 +28,14 @@
|
||||
|
||||
export let space: Ref<Team>
|
||||
export let parent: Ref<Issue> | undefined
|
||||
export let issueStatus = IssueStatus.Backlog
|
||||
export let status: IssueStatus = IssueStatus.Backlog
|
||||
export let priority: IssuePriority = IssuePriority.NoPriority
|
||||
export let assignee: Ref<Employee> | null = null
|
||||
|
||||
$: _space = space
|
||||
$: _parent = parent
|
||||
|
||||
let assignee: Ref<Employee> | null = null
|
||||
let currentAssignee: Ref<Employee> | null = assignee
|
||||
|
||||
const object: Data<Issue> = {
|
||||
title: '',
|
||||
@ -40,8 +43,8 @@
|
||||
assignee: null,
|
||||
number: 0,
|
||||
rank: '',
|
||||
status: issueStatus,
|
||||
priority: IssuePriority.NoPriority,
|
||||
status: status,
|
||||
priority: priority,
|
||||
dueDate: null,
|
||||
comments: 0
|
||||
}
|
||||
@ -87,7 +90,7 @@
|
||||
const value: Data<Issue> = {
|
||||
title: object.title,
|
||||
description: object.description,
|
||||
assignee,
|
||||
assignee: currentAssignee,
|
||||
number: (incResult as any).object.sequence,
|
||||
status: object.status,
|
||||
priority: object.priority,
|
||||
@ -101,7 +104,7 @@
|
||||
}
|
||||
|
||||
const moreActions: Array<{ icon: Asset; label: IntlString }> = [
|
||||
{ icon: tracker.icon.DueDate, label: tracker.string.DueDate },
|
||||
{ icon: tracker.icon.DueDate, label: tracker.string.SetDueDate },
|
||||
{ icon: tracker.icon.Parent, label: tracker.string.Parent }
|
||||
]
|
||||
|
||||
@ -138,7 +141,14 @@
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="space">
|
||||
<Button icon={tracker.icon.Home} label={presentation.string.Save} size={'small'} kind={'no-border'} disabled on:click={() => { }} />
|
||||
<Button
|
||||
icon={tracker.icon.Home}
|
||||
label={presentation.string.Save}
|
||||
size={'small'}
|
||||
kind={'no-border'}
|
||||
disabled
|
||||
on:click={() => {}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<EditBox
|
||||
bind:value={object.title}
|
||||
@ -160,7 +170,7 @@
|
||||
_class={contact.class.Employee}
|
||||
label={tracker.string.Assignee}
|
||||
placeholder={tracker.string.AssignTo}
|
||||
bind:value={assignee}
|
||||
bind:value={currentAssignee}
|
||||
allowDeselect
|
||||
titleDeselect={tracker.string.Unassigned}
|
||||
/>
|
||||
@ -190,6 +200,6 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button icon={IconAttachment} kind={'transparent'} on:click={() => { }} />
|
||||
<Button icon={IconAttachment} kind={'transparent'} on:click={() => {}} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { Label } from '@anticrm/ui'
|
||||
|
||||
export let items: Record<any, IntlString>
|
||||
export let selected: any | undefined = undefined
|
||||
|
||||
$: dropdownItems = Object.entries(items)
|
||||
</script>
|
||||
|
||||
<select class="dropdownNativeRoot" bind:value={selected}>
|
||||
{#each dropdownItems as [key, value]}
|
||||
<option value={key}>
|
||||
<Label label={value} />
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdownNativeRoot {
|
||||
justify-content: flex-start;
|
||||
border: none;
|
||||
min-width: 7rem;
|
||||
height: 1.5rem;
|
||||
padding: 0 1.1rem 0 0.5rem;
|
||||
background-color: var(--popup-divider);
|
||||
color: var(--caption-color);
|
||||
border-radius: 0.25rem;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--button-border-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -22,6 +22,7 @@
|
||||
export let kind: 'button' | 'icon' = 'button'
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let onPriorityChange: ((newPriority: IssuePriority | undefined) => void) | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
|
||||
const prioritiesInfo = [
|
||||
IssuePriority.NoPriority,
|
||||
@ -32,6 +33,9 @@
|
||||
].map((p) => ({ id: p, ...issuePriorities[p] }))
|
||||
|
||||
const handlePriorityEditorOpened = (event: MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: prioritiesInfo, placeholder: tracker.string.SetPriority, searchable: true },
|
||||
@ -51,12 +55,12 @@
|
||||
on:click={handlePriorityEditorOpened}
|
||||
/>
|
||||
{:else if kind === 'icon'}
|
||||
<div class="flex-presenter" on:click={handlePriorityEditorOpened}>
|
||||
<div class="priorityIcon">
|
||||
<div class={isEditable ? 'flex-presenter' : 'presenter'} on:click={handlePriorityEditorOpened}>
|
||||
<div class="priorityIcon" class:mPriorityIconEditable={isEditable}>
|
||||
<Icon icon={issuePriorities[priority].icon} size={'small'} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap">
|
||||
<div class="label nowrap ml-2">
|
||||
<Label label={issuePriorities[priority].label} />
|
||||
</div>
|
||||
{/if}
|
||||
@ -64,13 +68,21 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.presenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.priorityIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
&.mPriorityIconEditable {
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -22,6 +22,7 @@
|
||||
export let kind: 'button' | 'icon' = 'button'
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let onStatusChange: ((newStatus: IssueStatus | undefined) => void) | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
|
||||
const statusesInfo = [
|
||||
IssueStatus.Backlog,
|
||||
@ -32,6 +33,9 @@
|
||||
].map((s) => ({ id: s, ...issueStatuses[s] }))
|
||||
|
||||
const handleStatusEditorOpened = (event: MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
|
||||
@ -51,12 +55,12 @@
|
||||
on:click={handleStatusEditorOpened}
|
||||
/>
|
||||
{:else if kind === 'icon'}
|
||||
<div class="flex-presenter" on:click={handleStatusEditorOpened}>
|
||||
<div class={isEditable ? 'flex-presenter' : 'presenter'} on:click={handleStatusEditorOpened}>
|
||||
<div class="statusIcon">
|
||||
<Icon icon={issueStatuses[status].icon} size={'small'} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap">
|
||||
<div class="label nowrap ml-2">
|
||||
<Label label={issueStatuses[status].label} />
|
||||
</div>
|
||||
{/if}
|
||||
@ -64,6 +68,12 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.presenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
@ -7,4 +7,8 @@
|
||||
export let currentSpace: Ref<Team>
|
||||
</script>
|
||||
|
||||
<Issues {currentSpace} categories={[IssueStatus.InProgress, IssueStatus.Todo]} title={tracker.string.ActiveIssues} />
|
||||
<Issues
|
||||
{currentSpace}
|
||||
includedGroups={{ status: [IssueStatus.InProgress, IssueStatus.Todo] }}
|
||||
title={tracker.string.ActiveIssues}
|
||||
/>
|
||||
|
@ -20,18 +20,60 @@
|
||||
import { eventToHTMLElement, showPopup, Tooltip } from '@anticrm/ui'
|
||||
|
||||
import tracker from '../../plugin'
|
||||
import { IntlString, translate } from '@anticrm/platform'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
|
||||
$: employee = value?.$lookup?.assignee as Employee | undefined
|
||||
$: avatar = employee?.avatar
|
||||
$: formattedName = employee?.name ? formatName(employee.name) : ''
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
export let defaultName: IntlString | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let defaultNameString: string = ''
|
||||
let assignee: Employee | undefined = undefined
|
||||
|
||||
$: employee = (value?.$lookup?.assignee ?? assignee) as Employee | undefined
|
||||
$: avatar = employee?.avatar
|
||||
$: formattedName = employee?.name ? formatName(employee.name) : defaultNameString
|
||||
$: label = employee ? tracker.string.AssignedTo : tracker.string.AssignTo
|
||||
|
||||
$: findEmployeeById(value.assignee)
|
||||
$: getDefaultNameString = async () => {
|
||||
if (!defaultName) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await translate(defaultName, {})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
defaultNameString = result
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getDefaultNameString()
|
||||
})
|
||||
|
||||
const findEmployeeById = async (id: Ref<Employee> | null) => {
|
||||
if (!id) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const current = await client.findOne(contact.class.Employee, { _id: id })
|
||||
|
||||
if (current === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
assignee = current
|
||||
}
|
||||
|
||||
const handleAssigneeChanged = async (result: Employee | null | undefined) => {
|
||||
if (result === undefined) {
|
||||
if (!isEditable || result === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -47,6 +89,9 @@
|
||||
}
|
||||
|
||||
const handleAssigneeEditorOpened = async (event: MouseEvent) => {
|
||||
if (!isEditable) {
|
||||
return
|
||||
}
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
{
|
||||
@ -61,10 +106,36 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tooltip label={employee ? tracker.string.AssignedTo : tracker.string.AssignTo} props={{ value: formattedName }}>
|
||||
<div class="flex-presenter" on:click={handleAssigneeEditorOpened}>
|
||||
{#if isEditable}
|
||||
<Tooltip {label} props={{ value: formattedName }}>
|
||||
<div class="flex-presenter" on:click={handleAssigneeEditorOpened}>
|
||||
<div class="icon">
|
||||
<Avatar size={'x-small'} {avatar} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap ml-2">
|
||||
{formattedName}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<div class="presenter">
|
||||
<div class="icon">
|
||||
<Avatar size={'x-small'} {avatar} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap ml-2">
|
||||
{formattedName}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.presenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
@ -7,4 +7,4 @@
|
||||
export let currentSpace: Ref<Team>
|
||||
</script>
|
||||
|
||||
<Issues title={tracker.string.BacklogIssues} {currentSpace} categories={[IssueStatus.Backlog]} />
|
||||
<Issues title={tracker.string.BacklogIssues} {currentSpace} includedGroups={{ status: [IssueStatus.Backlog] }} />
|
||||
|
@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import { DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Button, eventToHTMLElement, Icon, IconAdd, Label, Scroller, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import { Component, Button, eventToHTMLElement, IconAdd, Scroller, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { issueStatuses } from '../../utils'
|
||||
import { IssuesGroupByKeys, IssuesOrderByKeys, issuesGroupPresenterMap, issuesSortOrderMap } from '../../utils'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import IssuesList from './IssuesList.svelte'
|
||||
|
||||
export let query: DocumentQuery<Issue>
|
||||
export let category: IssueStatus
|
||||
export let groupBy: { key: IssuesGroupByKeys | undefined; group: Issue[IssuesGroupByKeys] | undefined }
|
||||
export let orderBy: IssuesOrderByKeys
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let currentTeam: Team
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const options: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee
|
||||
@ -24,28 +24,40 @@
|
||||
|
||||
let issuesAmount = 0
|
||||
|
||||
$: grouping = groupBy.key !== undefined && groupBy.group !== undefined ? { [groupBy.key]: groupBy.group } : {}
|
||||
$: headerComponent = groupBy.key !== undefined ? issuesGroupPresenterMap[groupBy.key] : null
|
||||
|
||||
const handleNewIssueAdded = (event: MouseEvent) => {
|
||||
if (!currentSpace) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(CreateIssue, { space: currentSpace, issueStatus: category }, eventToHTMLElement(event))
|
||||
showPopup(CreateIssue, { space: currentSpace, ...grouping }, eventToHTMLElement(event))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="category" class:visible={issuesAmount > 0}>
|
||||
<div class="header categoryHeader flex-between label">
|
||||
<div class="flex-row-center gap-2">
|
||||
<Icon icon={issueStatuses[category].icon} size={'small'} />
|
||||
<span class="lines-limit-2"><Label label={issueStatuses[category].label} /></span>
|
||||
<span class="eLabelCounter ml-2">{issuesAmount}</span>
|
||||
{#if headerComponent}
|
||||
<div class="header categoryHeader flex-between label">
|
||||
<div class="flex-row-center gap-2">
|
||||
<Component
|
||||
is={headerComponent}
|
||||
props={{
|
||||
isEditable: false,
|
||||
shouldShowLabel: true,
|
||||
value: grouping,
|
||||
defaultName: groupBy.key === 'assignee' ? tracker.string.NoAssignee : undefined
|
||||
}}
|
||||
/>
|
||||
<span class="eLabelCounter ml-2">{issuesAmount}</span>
|
||||
</div>
|
||||
<div class="flex mr-1">
|
||||
<Tooltip label={tracker.string.AddIssueTooltip} direction={'left'}>
|
||||
<Button icon={IconAdd} kind={'transparent'} on:click={handleNewIssueAdded} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mr-1">
|
||||
<Tooltip label={tracker.string.AddIssueTooltip} direction={'left'}>
|
||||
<Button icon={IconAdd} kind={'transparent'} on:click={handleNewIssueAdded} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Scroller>
|
||||
<IssuesList
|
||||
_class={tracker.class.Issue}
|
||||
@ -60,8 +72,8 @@
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||
{ key: '', presenter: tracker.component.AssigneePresenter, props: { currentSpace } }
|
||||
]}
|
||||
{options}
|
||||
query={{ ...query, status: category }}
|
||||
options={{ ...options, sort: { [orderBy]: issuesSortOrderMap[orderBy] } }}
|
||||
query={{ ...query, ...grouping }}
|
||||
on:content={(evt) => {
|
||||
issuesAmount = evt.detail.length
|
||||
dispatch('content', issuesAmount)
|
||||
@ -81,7 +93,7 @@
|
||||
.categoryHeader {
|
||||
height: 2.5rem;
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
padding-left: 2rem;
|
||||
padding-left: 2.3rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
|
@ -13,59 +13,134 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import type { DocumentQuery, Ref } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { Label, ScrollBox } from '@anticrm/ui'
|
||||
import { Issue, Team, IssuesGrouping, IssuesOrdering } from '@anticrm/tracker'
|
||||
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import CategoryPresenter from './CategoryPresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
|
||||
import { IssuesGroupByKeys, issuesGroupKeyMap, issuesOrderKeyMap } from '../../utils'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
export let categories = [
|
||||
IssueStatus.InProgress,
|
||||
IssueStatus.Todo,
|
||||
IssueStatus.Backlog,
|
||||
IssueStatus.Done,
|
||||
IssueStatus.Canceled
|
||||
]
|
||||
export let title: IntlString = tracker.string.AllIssues
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let search: string = ''
|
||||
export let groupingKey: IssuesGrouping = IssuesGrouping.Status
|
||||
export let orderingKey: IssuesOrdering = IssuesOrdering.LastUpdated
|
||||
export let includedGroups: Partial<Record<IssuesGroupByKeys, Array<any>>> = {}
|
||||
|
||||
const ENTRIES_LIMIT = 200
|
||||
const spaceQuery = createQuery()
|
||||
const issuesQuery = createQuery()
|
||||
const issuesMap: { [status: string]: number } = {}
|
||||
let currentTeam: Team | undefined
|
||||
let issues: Issue[] = []
|
||||
|
||||
$: getTotalIssues = () => {
|
||||
$: totalIssues = getTotalIssues(issuesMap)
|
||||
|
||||
$: resultQuery =
|
||||
search === ''
|
||||
? { space: currentSpace, ...includedIssuesQuery, ...query }
|
||||
: { $search: search, space: currentSpace, ...includedIssuesQuery, ...query }
|
||||
|
||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
||||
currentTeam = res.shift()
|
||||
})
|
||||
|
||||
$: groupByKey = issuesGroupKeyMap[groupingKey]
|
||||
$: categories = getCategories(groupByKey, issues)
|
||||
$: displayedCategories = (categories as any[]).filter((x: ReturnType<typeof getCategories>) => {
|
||||
return (
|
||||
groupByKey === undefined || includedGroups[groupByKey] === undefined || includedGroups[groupByKey]?.includes(x)
|
||||
)
|
||||
})
|
||||
$: includedIssuesQuery = getIncludedIssues(includedGroups)
|
||||
|
||||
const getIncludedIssues = (groups: Partial<Record<IssuesGroupByKeys, Array<any>>>) => {
|
||||
const resultMap: { [p: string]: { $in: any[] } } = {}
|
||||
|
||||
for (const [key, value] of Object.entries(groups)) {
|
||||
resultMap[key] = { $in: value }
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
$: issuesQuery.query<Issue>(
|
||||
tracker.class.Issue,
|
||||
{ ...includedIssuesQuery },
|
||||
(result) => {
|
||||
issues = result
|
||||
},
|
||||
{ limit: ENTRIES_LIMIT, lookup: { assignee: contact.class.Employee } }
|
||||
)
|
||||
|
||||
const getCategories = (key: IssuesGroupByKeys | undefined, elements: Issue[]) => {
|
||||
if (!key) {
|
||||
return [undefined]
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
elements.map((x) => {
|
||||
return x[key]
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const getTotalIssues = (map: { [status: string]: number }) => {
|
||||
let total = 0
|
||||
|
||||
for (const issuesAmount of Object.values(issuesMap)) {
|
||||
total += issuesAmount
|
||||
for (const amount of Object.values(map)) {
|
||||
total += amount
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
$: resultQuery =
|
||||
search === '' ? { space: currentSpace, ...query } : { $search: search, space: currentSpace, ...query }
|
||||
const handleOptionsUpdated = (result: { orderBy: IssuesOrdering; groupBy: IssuesGrouping } | undefined) => {
|
||||
if (result === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTeam: Team | undefined
|
||||
for (const prop of Object.getOwnPropertyNames(issuesMap)) {
|
||||
delete issuesMap[prop]
|
||||
}
|
||||
|
||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
||||
currentTeam = res.shift()
|
||||
})
|
||||
groupingKey = result.groupBy
|
||||
orderingKey = result.orderBy
|
||||
}
|
||||
|
||||
const handleOptionsEditorOpened = (event: MouseEvent) => {
|
||||
if (!currentSpace) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(
|
||||
ViewOptionsPopup,
|
||||
{ groupBy: groupingKey, orderBy: orderingKey },
|
||||
eventToHTMLElement(event),
|
||||
undefined,
|
||||
handleOptionsUpdated
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if currentTeam}
|
||||
<ScrollBox vertical stretch>
|
||||
<div class="fs-title">
|
||||
<Label label={title} params={{ value: getTotalIssues() }} />
|
||||
<div class="fs-title flex-between mt-1 mr-1 ml-1">
|
||||
<Label label={title} params={{ value: totalIssues }} />
|
||||
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{#each categories as category}
|
||||
{#each displayedCategories as category}
|
||||
<CategoryPresenter
|
||||
{category}
|
||||
groupBy={{ key: groupByKey, group: category }}
|
||||
orderBy={issuesOrderKeyMap[orderingKey]}
|
||||
query={resultQuery}
|
||||
{currentSpace}
|
||||
{currentTeam}
|
||||
|
@ -14,7 +14,6 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, getObjectValue } from '@anticrm/core'
|
||||
import { SortingOrder } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { CheckBox, Loading, showPopup, Spinner, IconMoreV, Tooltip } from '@anticrm/ui'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
@ -36,7 +35,6 @@
|
||||
|
||||
const DOCS_MAX_AMOUNT = 200
|
||||
const liveQuery = createQuery()
|
||||
const sort = { modifiedOn: SortingOrder.Descending }
|
||||
|
||||
let selectedIssueIds = new Set<Ref<Doc>>()
|
||||
let selectedRowIndex: number | undefined
|
||||
@ -61,7 +59,7 @@
|
||||
dispatch('content', docObjects)
|
||||
isLoading = false
|
||||
},
|
||||
{ sort, ...options, limit: DOCS_MAX_AMOUNT }
|
||||
{ ...options, limit: DOCS_MAX_AMOUNT }
|
||||
)
|
||||
}
|
||||
|
||||
@ -239,9 +237,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.03rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(247, 248, 248, 0.5);
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
|
@ -22,11 +22,13 @@
|
||||
|
||||
export let value: Issue
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handlePriorityChanged = async (newPriority: IssuePriority | undefined) => {
|
||||
if (newPriority === undefined) {
|
||||
if (!isEditable || newPriority === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -41,12 +43,22 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SetPriority}>
|
||||
{#if isEditable}
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SetPriority}>
|
||||
<PrioritySelector
|
||||
kind={'icon'}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
priority={value.priority}
|
||||
onPriorityChange={handlePriorityChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<PrioritySelector
|
||||
kind={'icon'}
|
||||
shouldShowLabel={false}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
priority={value.priority}
|
||||
onPriorityChange={handlePriorityChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -22,11 +22,13 @@
|
||||
|
||||
export let value: Issue
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handleStatusChanged = async (newStatus: IssueStatus | undefined) => {
|
||||
if (newStatus === undefined) {
|
||||
if (!isEditable || newStatus === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -41,7 +43,23 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SetStatus}>
|
||||
<StatusSelector kind={'icon'} shouldShowLabel={false} status={value.status} onStatusChange={handleStatusChanged} />
|
||||
</Tooltip>
|
||||
{#if isEditable}
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SetStatus}>
|
||||
<StatusSelector
|
||||
kind={'icon'}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
status={value.status}
|
||||
onStatusChange={handleStatusChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<StatusSelector
|
||||
kind={'icon'}
|
||||
{isEditable}
|
||||
{shouldShowLabel}
|
||||
status={value.status}
|
||||
onStatusChange={handleStatusChanged}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -0,0 +1,86 @@
|
||||
<!--
|
||||
// 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 { IssuesGrouping, IssuesOrdering } from '@anticrm/tracker'
|
||||
import { Label } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { issuesGroupByOptions, issuesOrderByOptions } from '../../utils'
|
||||
import DropdownNative from '../DropdownNative.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let groupBy: IssuesGrouping | undefined = undefined
|
||||
export let orderBy: IssuesOrdering | undefined = undefined
|
||||
|
||||
const groupByItems = issuesGroupByOptions
|
||||
const orderByItems = issuesOrderByOptions
|
||||
|
||||
$: dispatch('update', { groupBy, orderBy })
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="sortingContainer">
|
||||
<div class="viewOption">
|
||||
<div class="label">
|
||||
<Label label={tracker.string.Grouping} />
|
||||
</div>
|
||||
<div class="dropdownContainer">
|
||||
<DropdownNative items={groupByItems} bind:selected={groupBy} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="viewOption">
|
||||
<div class="label">
|
||||
<Label label={tracker.string.Ordering} />
|
||||
</div>
|
||||
<div class="dropdownContainer">
|
||||
<DropdownNative items={orderByItems} bind:selected={orderBy} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 17rem;
|
||||
background-color: var(--board-card-bg-color);
|
||||
}
|
||||
|
||||
.sortingContainer {
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--popup-divider);
|
||||
}
|
||||
|
||||
.viewOption {
|
||||
display: flex;
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 5rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
@ -31,6 +31,7 @@ import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
|
||||
import StatusPresenter from './components/issues/StatusPresenter.svelte'
|
||||
import DueDatePresenter from './components/issues/DueDatePresenter.svelte'
|
||||
import AssigneePresenter from './components/issues/AssigneePresenter.svelte'
|
||||
import ViewOptionsPopup from './components/issues/ViewOptionsPopup.svelte'
|
||||
|
||||
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
|
||||
import EditIssue from './components/issues/EditIssue.svelte'
|
||||
@ -55,6 +56,7 @@ export default async (): Promise<Resources> => ({
|
||||
AssigneePresenter,
|
||||
DueDatePresenter,
|
||||
EditIssue,
|
||||
NewIssueHeader
|
||||
NewIssueHeader,
|
||||
ViewOptionsPopup
|
||||
}
|
||||
})
|
||||
|
@ -70,7 +70,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
Attachments: '' as IntlString,
|
||||
Labels: '' as IntlString,
|
||||
Space: '' as IntlString,
|
||||
DueDate: '' as IntlString,
|
||||
SetDueDate: '' as IntlString,
|
||||
ModificationDate: '' as IntlString,
|
||||
Issue: '' as IntlString,
|
||||
Document: '' as IntlString,
|
||||
@ -81,6 +81,12 @@ export default mergeIds(trackerId, tracker, {
|
||||
DueDatePopupOverdueTitle: '' as IntlString,
|
||||
DueDatePopupDescription: '' as IntlString,
|
||||
DueDatePopupOverdueDescription: '' as IntlString,
|
||||
Grouping: '' as IntlString,
|
||||
Ordering: '' as IntlString,
|
||||
NoGrouping: '' as IntlString,
|
||||
NoAssignee: '' as IntlString,
|
||||
LastUpdated: '' as IntlString,
|
||||
DueDate: '' as IntlString,
|
||||
|
||||
IssueTitlePlaceholder: '' as IntlString,
|
||||
IssueDescriptionPlaceholder: '' as IntlString,
|
||||
|
@ -14,9 +14,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { Ref, SortingOrder } from '@anticrm/core'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { IssuePriority, IssueStatus, Team, IssuesGrouping, IssuesOrdering, Issue } from '@anticrm/tracker'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
||||
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
||||
@ -78,3 +78,47 @@ export const issuePriorities: Record<IssuePriority, { icon: Asset, label: IntlSt
|
||||
[IssuePriority.Medium]: { icon: tracker.icon.PriorityMedium, label: tracker.string.Medium },
|
||||
[IssuePriority.Low]: { icon: tracker.icon.PriorityLow, label: tracker.string.Low }
|
||||
}
|
||||
|
||||
export const issuesGroupByOptions: Record<IssuesGrouping, IntlString> = {
|
||||
[IssuesGrouping.Status]: tracker.string.Status,
|
||||
[IssuesGrouping.Assignee]: tracker.string.Assignee,
|
||||
[IssuesGrouping.Priority]: tracker.string.Priority,
|
||||
[IssuesGrouping.NoGrouping]: tracker.string.NoGrouping
|
||||
}
|
||||
|
||||
export const issuesOrderByOptions: Record<IssuesOrdering, IntlString> = {
|
||||
[IssuesOrdering.Status]: tracker.string.Status,
|
||||
[IssuesOrdering.Priority]: tracker.string.Priority,
|
||||
[IssuesOrdering.LastUpdated]: tracker.string.LastUpdated,
|
||||
[IssuesOrdering.DueDate]: tracker.string.DueDate
|
||||
}
|
||||
|
||||
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' >
|
||||
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'>
|
||||
|
||||
export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = {
|
||||
[IssuesGrouping.Status]: 'status',
|
||||
[IssuesGrouping.Priority]: 'priority',
|
||||
[IssuesGrouping.Assignee]: 'assignee',
|
||||
[IssuesGrouping.NoGrouping]: undefined
|
||||
}
|
||||
|
||||
export const issuesOrderKeyMap: Record<IssuesOrdering, IssuesOrderByKeys> = {
|
||||
[IssuesOrdering.Status]: 'status',
|
||||
[IssuesOrdering.Priority]: 'priority',
|
||||
[IssuesOrdering.LastUpdated]: 'modifiedOn',
|
||||
[IssuesOrdering.DueDate]: 'dueDate'
|
||||
}
|
||||
|
||||
export const issuesSortOrderMap: Record<IssuesOrderByKeys, SortingOrder> = {
|
||||
status: SortingOrder.Ascending,
|
||||
priority: SortingOrder.Ascending,
|
||||
modifiedOn: SortingOrder.Descending,
|
||||
dueDate: SortingOrder.Descending
|
||||
}
|
||||
|
||||
export const issuesGroupPresenterMap: Record<IssuesGroupByKeys, AnyComponent | undefined> = {
|
||||
status: tracker.component.StatusPresenter,
|
||||
priority: tracker.component.PriorityPresenter,
|
||||
assignee: tracker.component.AssigneePresenter
|
||||
}
|
||||
|
@ -49,6 +49,26 @@ export enum IssuePriority {
|
||||
Low
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export enum IssuesGrouping {
|
||||
Status = 'status',
|
||||
Assignee = 'assignee',
|
||||
Priority = 'priority',
|
||||
NoGrouping = 'noGrouping'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export enum IssuesOrdering {
|
||||
Status = 'status',
|
||||
Priority = 'priority',
|
||||
LastUpdated = 'lastUpdated',
|
||||
DueDate = 'dueDate'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user