mirror of
https://github.com/hcengineering/platform.git
synced 2025-02-08 03:47:20 +00:00
332 lines
10 KiB
Svelte
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}
|