Tracker: View options - Grouping (#1442)

Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
Artyom Grigorovich 2022-04-20 13:06:05 +07:00 committed by GitHub
parent c24a27c2ba
commit e4e39469c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 523 additions and 91 deletions

View 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>

View File

@ -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'

View File

@ -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": {}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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}
/>

View File

@ -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>

View File

@ -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] }} />

View File

@ -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 {

View File

@ -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}

View File

@ -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 {

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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
}
})

View File

@ -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,

View File

@ -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
}

View File

@ -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
*/