platform/plugins/tracker-resources/src/components/issues/Issues.svelte
Artyom Grigorovich 38a8940a8b
Tracker: Add keyboard support for issues list (#1539)
Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
2022-04-26 15:46:35 +07:00

332 lines
10 KiB
Svelte

<!--
// 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, { Employee } from '@anticrm/contact'
import { DocumentQuery, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import {
Issue,
Team,
IssuesGrouping,
IssuesOrdering,
IssuesDateModificationPeriod,
IssueStatus,
IssueStatusCategory,
IssuePriority
} from '@anticrm/tracker'
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
import tracker from '../../plugin'
import { IntlString } from '@anticrm/platform'
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
import {
IssuesGroupByKeys,
issuesGroupKeyMap,
issuesOrderKeyMap,
getIssuesModificationDatePeriodTime,
groupBy,
issuesSortOrderMap
} from '../../utils'
import IssuesListBrowser from './IssuesListBrowser.svelte'
export let currentSpace: Ref<Team>
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 completedIssuesPeriod: IssuesDateModificationPeriod | null = IssuesDateModificationPeriod.All
export let shouldShowEmptyGroups: boolean | undefined = false
export let includedGroups: Partial<Record<IssuesGroupByKeys, Array<any>>> = {}
const ENTRIES_LIMIT = 200
const spaceQuery = createQuery()
const issuesQuery = createQuery()
const resultIssuesQuery = createQuery()
const statusesQuery = createQuery()
const issuesMap: { [status: string]: number } = {}
let currentTeam: Team | undefined
let issues: Issue[] = []
let resultIssues: Issue[] = []
let statusesById: ReadonlyMap<Ref<IssueStatus>, WithLookup<IssueStatus>> = new Map()
let employees: (WithLookup<Employee> | undefined)[] = []
$: totalIssues = issues.length
const options: FindOptions<Issue> = {
sort: { [issuesOrderKeyMap[orderingKey]]: issuesSortOrderMap[issuesOrderKeyMap[orderingKey]] },
limit: ENTRIES_LIMIT,
lookup: { assignee: contact.class.Employee, status: tracker.class.IssueStatus }
}
$: baseQuery = {
space: currentSpace,
...includedIssuesQuery,
...filteredIssuesQuery,
...query
}
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
currentTeam = res.shift()
})
$: groupByKey = issuesGroupKeyMap[groupingKey]
$: categories = getCategories(groupByKey, resultIssues, !!shouldShowEmptyGroups)
$: groupedIssues = getGroupedIssues(groupByKey, resultIssues, categories)
$: displayedCategories = (categories as any[]).filter((x) => {
if (groupByKey === undefined || includedGroups[groupByKey] === undefined) {
return true
}
if (groupByKey === 'status') {
const category = statusesById.get(x as Ref<IssueStatus>)?.category
return !!(category && includedGroups.status?.includes(category))
}
return includedGroups[groupByKey]?.includes(x)
})
$: includedIssuesQuery = getIncludedIssuesQuery(includedGroups, statuses)
$: filteredIssuesQuery = getModifiedOnIssuesFilterQuery(issues, completedIssuesPeriod)
$: statuses = [...statusesById.values()]
const getIncludedIssuesQuery = (
groups: Partial<Record<IssuesGroupByKeys, Array<any>>>,
issueStatuses: IssueStatus[]
) => {
const resultMap: { [p: string]: { $in: any[] } } = {}
for (const [key, value] of Object.entries(groups)) {
const includedCategories = key === 'status' ? filterIssueStatuses(issueStatuses, value) : value
resultMap[key] = { $in: includedCategories }
}
return resultMap
}
const getModifiedOnIssuesFilterQuery = (
currentIssues: WithLookup<Issue>[],
period: IssuesDateModificationPeriod | null
) => {
const filter: { _id: { $in: Array<Ref<Issue>> } } = { _id: { $in: [] } }
if (!period || period === IssuesDateModificationPeriod.All) {
return {}
}
for (const issue of currentIssues) {
if (
issue.$lookup?.status?.category === tracker.issueStatusCategory.Completed &&
issue.modifiedOn < getIssuesModificationDatePeriodTime(period)
) {
continue
}
filter._id.$in.push(issue._id)
}
return filter
}
$: issuesQuery.query<Issue>(
tracker.class.Issue,
{ ...includedIssuesQuery },
(result) => {
issues = result
employees = result.map((x) => x.$lookup?.assignee)
},
options
)
$: resultIssuesQuery.query<Issue>(
tracker.class.Issue,
{ ...resultQuery },
(result) => {
resultIssues = result
employees = result.map((x) => x.$lookup?.assignee)
},
options
)
$: statusesQuery.query(
tracker.class.IssueStatus,
{ attachedTo: currentSpace },
(issueStatuses) => {
statusesById = new Map(issueStatuses.map((status) => [status._id, status]))
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
const getGroupedIssues = (key: IssuesGroupByKeys | undefined, elements: Issue[], orderedCategories: any[]) => {
if (!groupByKey) {
return { [undefined as any]: issues }
}
const unorderedIssues = groupBy(elements, key)
return Object.keys(unorderedIssues)
.sort((o1, o2) => {
const i1 = orderedCategories.findIndex((x) => x === o1)
const i2 = orderedCategories.findIndex((x) => x === o2)
return i1 - i2
})
.reduce((obj: { [p: string]: any[] }, objKey) => {
obj[objKey] = unorderedIssues[objKey]
return obj
}, {})
}
const getCategories = (key: IssuesGroupByKeys | undefined, elements: Issue[], shouldShowAll: boolean) => {
if (!key) {
return [undefined] // No grouping
}
const defaultPriorities = [
IssuePriority.NoPriority,
IssuePriority.Urgent,
IssuePriority.High,
IssuePriority.Medium,
IssuePriority.Low
]
const defaultStatuses = Object.values(statuses).map((x) => x._id)
const existingCategories = Array.from(
new Set(
elements.map((x) => {
return x[key]
})
)
)
if (shouldShowAll) {
if (key === 'status') {
return defaultStatuses
}
if (key === 'priority') {
return defaultPriorities
}
}
if (key === 'status') {
existingCategories.sort((s1, s2) => {
const i1 = defaultStatuses.findIndex((x) => x === s1)
const i2 = defaultStatuses.findIndex((x) => x === s2)
return i1 - i2
})
}
if (key === 'priority') {
existingCategories.sort((p1, p2) => {
const i1 = defaultPriorities.findIndex((x) => x === p1)
const i2 = defaultPriorities.findIndex((x) => x === p2)
return i1 - i2
})
}
return existingCategories
}
function filterIssueStatuses (
issueStatuses: IssueStatus[],
issueStatusCategories: Ref<IssueStatusCategory>[]
): Ref<IssueStatus>[] {
const statusCategories = new Set(issueStatusCategories)
return issueStatuses.filter((status) => statusCategories.has(status.category)).map((s) => s._id)
}
const handleOptionsUpdated = (
result:
| {
orderBy: IssuesOrdering
groupBy: IssuesGrouping
completedIssuesPeriod: IssuesDateModificationPeriod
shouldShowEmptyGroups: boolean
}
| undefined
) => {
if (result === undefined) {
return
}
for (const prop of Object.getOwnPropertyNames(issuesMap)) {
delete issuesMap[prop]
}
groupingKey = result.groupBy
orderingKey = result.orderBy
completedIssuesPeriod = result.completedIssuesPeriod
shouldShowEmptyGroups = result.shouldShowEmptyGroups
if (result.groupBy === IssuesGrouping.Assignee || result.groupBy === IssuesGrouping.NoGrouping) {
shouldShowEmptyGroups = undefined
}
}
const handleOptionsEditorOpened = (event: MouseEvent) => {
if (!currentSpace) {
return
}
showPopup(
ViewOptionsPopup,
{ groupBy: groupingKey, orderBy: orderingKey, completedIssuesPeriod, shouldShowEmptyGroups },
eventToHTMLElement(event),
undefined,
handleOptionsUpdated
)
}
</script>
{#if currentTeam}
<ScrollBox vertical stretch>
<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">
<IssuesListBrowser
_class={tracker.class.Issue}
{currentSpace}
{groupByKey}
orderBy={issuesOrderKeyMap[orderingKey]}
{statuses}
{employees}
categories={displayedCategories}
itemsConfig={[
{ key: '', presenter: tracker.component.PriorityPresenter, props: { currentSpace } },
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
{ key: '', presenter: tracker.component.StatusPresenter, props: { currentSpace, statuses } },
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { currentSpace } },
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
{ key: '', presenter: tracker.component.AssigneePresenter, props: { currentSpace } }
]}
{groupedIssues}
/>
</div>
</ScrollBox>
{/if}