mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-16 21:35:10 +00:00
Tracker: Issue filters - additional features (#1708)
Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
parent
d7df2ff0a0
commit
1c98ca3c3a
@ -18,7 +18,7 @@
|
|||||||
import { FilterSectionElement } from '../utils'
|
import { FilterSectionElement } from '../utils'
|
||||||
|
|
||||||
export let actions: FilterSectionElement[] = []
|
export let actions: FilterSectionElement[] = []
|
||||||
export let onBack: () => void
|
export let onBack: (() => void) | undefined = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const actionElements: HTMLButtonElement[] = []
|
const actionElements: HTMLButtonElement[] = []
|
||||||
@ -28,7 +28,7 @@
|
|||||||
const getSelectedElementsMap = (actions: FilterSectionElement[]) => {
|
const getSelectedElementsMap = (actions: FilterSectionElement[]) => {
|
||||||
const result: { [k: number]: boolean } = {}
|
const result: { [k: number]: boolean } = {}
|
||||||
|
|
||||||
for (let i = 1; i < actions.length; ++i) {
|
for (let i = onBack ? 1 : 0; i < actions.length; ++i) {
|
||||||
result[i] = !!actions[i].isSelected
|
result[i] = !!actions[i].isSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,8 @@
|
|||||||
|
|
||||||
if (event.key === 'ArrowLeft') {
|
if (event.key === 'ArrowLeft') {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
onBack()
|
|
||||||
|
onBack?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,25 +75,27 @@
|
|||||||
event.currentTarget.focus()
|
event.currentTarget.focus()
|
||||||
}}
|
}}
|
||||||
on:click={(event) => {
|
on:click={(event) => {
|
||||||
if (i === 0) {
|
if (i === 0 && onBack) {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
action.onSelect(event)
|
action.onSelect(event)
|
||||||
|
|
||||||
if (i !== 0) {
|
if (i !== 0 || !onBack) {
|
||||||
selectedElementsMap[i] = !selectedElementsMap[i]
|
selectedElementsMap[i] = !selectedElementsMap[i]
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="buttonContent">
|
<div class="buttonContent">
|
||||||
{#if i !== 0}
|
{#if i !== 0 || (i === 0 && !onBack)}
|
||||||
<div class="flex check pointer-events-none">
|
<div class="flex check pointer-events-none">
|
||||||
<CheckBox checked={selectedElementsMap[i]} primary />
|
<CheckBox checked={selectedElementsMap[i]} primary />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if action.icon}
|
{#if action.icon}
|
||||||
<div class="icon" class:ml-3={i > 0}><Icon icon={action.icon} size={'small'} /></div>
|
<div class="icon" class:ml-3={i > 0 || (i === 0 && !onBack)}>
|
||||||
|
<Icon icon={action.icon} size={'small'} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if action.title}
|
{#if action.title}
|
||||||
<div class="ml-3 pr-1">{action.title}</div>
|
<div class="ml-3 pr-1">{action.title}</div>
|
||||||
|
@ -13,26 +13,103 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, IconAdd } from '@anticrm/ui'
|
import { Button, eventToHTMLElement, IconAdd, showPopup } from '@anticrm/ui'
|
||||||
|
|
||||||
import FilterSummarySection from './FilterSummarySection.svelte'
|
import FilterSummarySection from './FilterSummarySection.svelte'
|
||||||
|
import StatusFilterMenuSection from './issues/StatusFilterMenuSection.svelte'
|
||||||
|
import PriorityFilterMenuSection from './issues/PriorityFilterMenuSection.svelte'
|
||||||
|
import { defaultPriorities, getGroupedIssues, IssueFilter } from '../utils'
|
||||||
|
import { WithLookup } from '@anticrm/core'
|
||||||
|
import { Issue, IssueStatus } from '@anticrm/tracker'
|
||||||
|
|
||||||
export let filters: { [p: string]: any[] } = {}
|
export let filters: IssueFilter[] = []
|
||||||
export let onDeleteFilter: (filterKey?: string) => void
|
export let issues: Issue[] = []
|
||||||
|
export let defaultStatuses: Array<WithLookup<IssueStatus>> = []
|
||||||
|
export let onUpdateFilter: (result: { [p: string]: any }, filterIndex: number) => void
|
||||||
|
export let onAddFilter: ((event: MouseEvent) => void) | undefined = undefined
|
||||||
|
export let onDeleteFilter: (filterIndex?: number) => void
|
||||||
|
export let onChangeMode: (index: number) => void
|
||||||
|
|
||||||
|
$: defaultStatusIds = defaultStatuses.map((x) => x._id)
|
||||||
|
$: groupedByStatus = getGroupedIssues('status', issues, defaultStatusIds)
|
||||||
|
$: groupedByPriority = getGroupedIssues('priority', issues, defaultPriorities)
|
||||||
|
|
||||||
|
const handleEditFilterMenuOpened = (event: MouseEvent, type: string, index: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'status': {
|
||||||
|
const statusGroups: { [key: string]: number } = {}
|
||||||
|
|
||||||
|
for (const status of defaultStatuses) {
|
||||||
|
statusGroups[status._id] = groupedByStatus[status._id]?.length ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mode, query } = filters[index]
|
||||||
|
const currentFilterQuery = query as { [p: string]: { $in?: any[]; $nin?: any[] } } | undefined
|
||||||
|
|
||||||
|
showPopup(
|
||||||
|
StatusFilterMenuSection,
|
||||||
|
{
|
||||||
|
groups: statusGroups,
|
||||||
|
statuses: defaultStatuses,
|
||||||
|
selectedElements: currentFilterQuery?.status?.[mode] ?? [],
|
||||||
|
index,
|
||||||
|
onUpdate: onUpdateFilter
|
||||||
|
},
|
||||||
|
eventToHTMLElement(event)
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'priority': {
|
||||||
|
const priorityGroups: { [key: string]: number } = {}
|
||||||
|
|
||||||
|
for (const priority of defaultPriorities) {
|
||||||
|
priorityGroups[priority] = groupedByPriority[priority]?.length ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mode, query } = filters[index]
|
||||||
|
const currentFilterQuery = query as { [p: string]: { $in?: any[]; $nin?: any[] } } | undefined
|
||||||
|
|
||||||
|
showPopup(
|
||||||
|
PriorityFilterMenuSection,
|
||||||
|
{
|
||||||
|
groups: priorityGroups,
|
||||||
|
selectedElements: currentFilterQuery?.priority?.[mode] ?? [],
|
||||||
|
index,
|
||||||
|
onUpdate: onUpdateFilter
|
||||||
|
},
|
||||||
|
eventToHTMLElement(event)
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
{#each Object.entries(filters) as [key, value]}
|
{#each filters as filter, filterIndex}
|
||||||
<FilterSummarySection type={key} selectedFilters={value} onDelete={() => onDeleteFilter(key)} />
|
{@const [key, value] = Object.entries(filter.query)[0]}
|
||||||
|
<FilterSummarySection
|
||||||
|
type={key}
|
||||||
|
mode={filter.mode}
|
||||||
|
selectedFilters={value?.[filter.mode]}
|
||||||
|
onDelete={() => onDeleteFilter(filterIndex)}
|
||||||
|
onChangeMode={() => onChangeMode(filterIndex)}
|
||||||
|
onEditFilter={(event) => handleEditFilterMenuOpened(event, key, filterIndex)}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if onAddFilter}
|
||||||
<div class="ml-2">
|
<div class="ml-2">
|
||||||
<Button kind={'link'} icon={IconAdd} />
|
<Button kind={'link'} icon={IconAdd} on:click={onAddFilter} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.root {
|
.root {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-flow: row wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -19,8 +19,11 @@
|
|||||||
import tracker from '../plugin'
|
import tracker from '../plugin'
|
||||||
|
|
||||||
export let type: string = ''
|
export let type: string = ''
|
||||||
|
export let mode: '$in' | '$nin' = '$in'
|
||||||
export let selectedFilters: any[] = []
|
export let selectedFilters: any[] = []
|
||||||
export let onDelete: () => void
|
export let onDelete: () => void
|
||||||
|
export let onChangeMode: () => void
|
||||||
|
export let onEditFilter: (event: MouseEvent) => void
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
@ -30,7 +33,12 @@
|
|||||||
<div class="buttonWrapper">
|
<div class="buttonWrapper">
|
||||||
<Button
|
<Button
|
||||||
shape="rectangle"
|
shape="rectangle"
|
||||||
label={selectedFilters.length < 2 ? tracker.string.FilterIs : tracker.string.FilterIsEither}
|
label={mode === '$nin'
|
||||||
|
? tracker.string.FilterIsNot
|
||||||
|
: selectedFilters.length < 2
|
||||||
|
? tracker.string.FilterIs
|
||||||
|
: tracker.string.FilterIsEither}
|
||||||
|
on:click={onChangeMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttonWrapper">
|
<div class="buttonWrapper">
|
||||||
@ -38,6 +46,7 @@
|
|||||||
shape={'rectangle'}
|
shape={'rectangle'}
|
||||||
label={tracker.string.FilterStatesCount}
|
label={tracker.string.FilterStatesCount}
|
||||||
labelParams={{ value: selectedFilters.length }}
|
labelParams={{ value: selectedFilters.length }}
|
||||||
|
on:click={onEditFilter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttonWrapper">
|
<div class="buttonWrapper">
|
||||||
@ -49,6 +58,10 @@
|
|||||||
.root {
|
.root {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonWrapper {
|
.buttonWrapper {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
import { IssuePriority } from '@anticrm/tracker'
|
import { IssuePriority } from '@anticrm/tracker'
|
||||||
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||||
import { issuePriorities } from '../utils'
|
import { defaultPriorities, issuePriorities } from '../utils'
|
||||||
import tracker from '../plugin'
|
import tracker from '../plugin'
|
||||||
|
|
||||||
export let priority: IssuePriority
|
export let priority: IssuePriority
|
||||||
@ -29,13 +29,7 @@
|
|||||||
export let justify: 'left' | 'center' = 'center'
|
export let justify: 'left' | 'center' = 'center'
|
||||||
export let width: string | undefined = 'min-content'
|
export let width: string | undefined = 'min-content'
|
||||||
|
|
||||||
const prioritiesInfo = [
|
const prioritiesInfo = defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] }))
|
||||||
IssuePriority.NoPriority,
|
|
||||||
IssuePriority.Urgent,
|
|
||||||
IssuePriority.High,
|
|
||||||
IssuePriority.Medium,
|
|
||||||
IssuePriority.Low
|
|
||||||
].map((p) => ({ id: p, ...issuePriorities[p] }))
|
|
||||||
|
|
||||||
const handlePriorityEditorOpened = (event: MouseEvent) => {
|
const handlePriorityEditorOpened = (event: MouseEvent) => {
|
||||||
if (!isEditable) {
|
if (!isEditable) {
|
||||||
|
@ -23,8 +23,7 @@
|
|||||||
IssuesOrdering,
|
IssuesOrdering,
|
||||||
IssuesDateModificationPeriod,
|
IssuesDateModificationPeriod,
|
||||||
IssueStatus,
|
IssueStatus,
|
||||||
IssueStatusCategory,
|
IssueStatusCategory
|
||||||
IssuePriority
|
|
||||||
} from '@anticrm/tracker'
|
} from '@anticrm/tracker'
|
||||||
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement, IconAdd, IconClose } from '@anticrm/ui'
|
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement, IconAdd, IconClose } from '@anticrm/ui'
|
||||||
import { IntlString } from '@anticrm/platform'
|
import { IntlString } from '@anticrm/platform'
|
||||||
@ -40,7 +39,11 @@
|
|||||||
issuesOrderKeyMap,
|
issuesOrderKeyMap,
|
||||||
getIssuesModificationDatePeriodTime,
|
getIssuesModificationDatePeriodTime,
|
||||||
issuesSortOrderMap,
|
issuesSortOrderMap,
|
||||||
getGroupedIssues
|
getGroupedIssues,
|
||||||
|
defaultPriorities,
|
||||||
|
getArraysIntersection,
|
||||||
|
IssueFilter,
|
||||||
|
getArraysUnion
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
export let currentSpace: Ref<Team>
|
export let currentSpace: Ref<Team>
|
||||||
@ -62,7 +65,7 @@
|
|||||||
const issuesMap: { [status: string]: number } = {}
|
const issuesMap: { [status: string]: number } = {}
|
||||||
|
|
||||||
let filterElement: HTMLElement | null = null
|
let filterElement: HTMLElement | null = null
|
||||||
let filters: { [p: string]: any[] } = {}
|
let filters: IssueFilter[] = []
|
||||||
let currentTeam: Team | undefined
|
let currentTeam: Team | undefined
|
||||||
let issues: Issue[] = []
|
let issues: Issue[] = []
|
||||||
let resultIssues: Issue[] = []
|
let resultIssues: Issue[] = []
|
||||||
@ -71,7 +74,7 @@
|
|||||||
|
|
||||||
$: totalIssuesCount = issues.length
|
$: totalIssuesCount = issues.length
|
||||||
$: resultIssuesCount = resultIssues.length
|
$: resultIssuesCount = resultIssues.length
|
||||||
$: isFiltersEmpty = Object.keys(filters).length === 0
|
$: isFiltersEmpty = filters.length === 0
|
||||||
|
|
||||||
const options: FindOptions<Issue> = {
|
const options: FindOptions<Issue> = {
|
||||||
sort: { [issuesOrderKeyMap[orderingKey]]: issuesSortOrderMap[issuesOrderKeyMap[orderingKey]] },
|
sort: { [issuesOrderKeyMap[orderingKey]]: issuesSortOrderMap[issuesOrderKeyMap[orderingKey]] },
|
||||||
@ -189,13 +192,6 @@
|
|||||||
return [undefined] // No grouping
|
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 defaultStatuses = Object.values(statuses).map((x) => x._id)
|
||||||
|
|
||||||
const existingCategories = Array.from(
|
const existingCategories = Array.from(
|
||||||
@ -318,66 +314,104 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFiltersQuery = (filterParams: { [f: string]: any[] }) => {
|
const getFiltersQuery = (filters: IssueFilter[]) => {
|
||||||
const result: { [f: string]: { $in: any[] } } = {}
|
const result: { [f: string]: { $in?: any[]; $nin?: any[] } } = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(filterParams)) {
|
for (const filter of filters) {
|
||||||
result[key] = { $in: [...value] }
|
for (const [key, value] of Object.entries(filter.query)) {
|
||||||
|
const { mode } = filter
|
||||||
|
|
||||||
|
if (result[key] === undefined) {
|
||||||
|
result[key] = { ...value }
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[key][mode] === undefined) {
|
||||||
|
result[key][mode] = [...value[mode]]
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultFunction = mode === '$nin' ? getArraysUnion : getArraysIntersection
|
||||||
|
|
||||||
|
result[key][mode] = resultFunction(result[key]?.[mode] ?? [], value[mode])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFilterDeleted = (filterKey?: string) => {
|
const handleFilterDeleted = (filterIndex?: number) => {
|
||||||
if (filterKey) {
|
if (filterIndex !== undefined) {
|
||||||
delete filters[filterKey]
|
filters.splice(filterIndex, 1)
|
||||||
|
} else {
|
||||||
|
filters.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
filters = filters
|
filters = filters
|
||||||
} else {
|
|
||||||
filters = {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAllFiltersDeleted = () => {
|
const handleAllFiltersDeleted = () => {
|
||||||
handleFilterDeleted()
|
handleFilterDeleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFiltersModified = (result: { [p: string]: any }) => {
|
const handleFiltersModified = (result: { [p: string]: any }, index?: number) => {
|
||||||
|
const i = index === undefined ? filters.length : index
|
||||||
const entries = Object.entries(result)
|
const entries = Object.entries(result)
|
||||||
|
|
||||||
if (entries.length !== 1) {
|
if (entries.length !== 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const [filter, filterValue] = entries[0]
|
const [filterKey, filterValue] = entries[0]
|
||||||
|
|
||||||
if (filter in filters) {
|
if (filters[i]) {
|
||||||
if (filters[filter].includes(filterValue)) {
|
const { mode, query: currentFilterQuery } = filters[i]
|
||||||
filters[filter] = filters[filter].filter((x) => x !== filterValue)
|
const currentFilterQueryConditions: any[] = currentFilterQuery[filterKey]?.[mode] ?? []
|
||||||
|
|
||||||
if (Object.keys(filters).length === 1 && filters[filter].length === 0) {
|
if (currentFilterQueryConditions.includes(filterValue)) {
|
||||||
filters = {}
|
const updatedFilterConditions = currentFilterQueryConditions.filter((x: any) => x !== filterValue)
|
||||||
|
|
||||||
|
filters[i] = { mode, query: { [filterKey]: { [mode]: updatedFilterConditions } } }
|
||||||
|
|
||||||
|
if (filters.length === 1 && updatedFilterConditions.length === 0) {
|
||||||
|
filters.length = 0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
filters[filter] = [...filters[filter], filterValue]
|
filters[i] = { mode, query: { [filterKey]: { $in: [...currentFilterQueryConditions, filterValue] } } }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
filters[filter] = [filterValue]
|
filters[i] = { mode: '$in', query: { [filterKey]: { $in: [filterValue] } } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters = filters
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterModeChanged = (index: number) => {
|
||||||
|
if (!filters[index]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mode: currentMode, query: currentQuery } = filters[index]
|
||||||
|
const newMode = currentMode === '$in' ? '$nin' : '$in'
|
||||||
|
const [filterKey, filterValue] = Object.entries(currentQuery)[0]
|
||||||
|
|
||||||
|
filters[index] = { mode: newMode, query: { [filterKey]: { [newMode]: [...filterValue[currentMode]] } } }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFiltersBackButtonPressed = (event: MouseEvent) => {
|
const handleFiltersBackButtonPressed = (event: MouseEvent) => {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
|
|
||||||
handleFilterMenuOpened(event)
|
handleFilterMenuOpened(event, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFilterMenuOpened = (event: MouseEvent) => {
|
const handleFilterMenuOpened = (event: MouseEvent, shouldUpdateFilterTargetElement: boolean = true) => {
|
||||||
if (!currentSpace) {
|
if (!currentSpace) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterElement === null) {
|
if (!filterElement || shouldUpdateFilterTargetElement) {
|
||||||
filterElement = eventToHTMLElement(event)
|
filterElement = eventToHTMLElement(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,7 +419,8 @@
|
|||||||
IssuesFilterMenu,
|
IssuesFilterMenu,
|
||||||
{
|
{
|
||||||
issues,
|
issues,
|
||||||
currentFilter: getFiltersQuery(filters),
|
filters: filters,
|
||||||
|
index: filters.length,
|
||||||
defaultStatuses: statuses,
|
defaultStatuses: statuses,
|
||||||
onBack: handleFiltersBackButtonPressed,
|
onBack: handleFiltersBackButtonPressed,
|
||||||
targetHtml: filterElement,
|
targetHtml: filterElement,
|
||||||
@ -410,6 +445,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<Button
|
<Button
|
||||||
|
size="small"
|
||||||
icon={isFiltersEmpty ? IconAdd : IconClose}
|
icon={isFiltersEmpty ? IconAdd : IconClose}
|
||||||
kind={'link-bordered'}
|
kind={'link-bordered'}
|
||||||
borderStyle={'dashed'}
|
borderStyle={'dashed'}
|
||||||
@ -420,9 +456,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
|
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
|
||||||
</div>
|
</div>
|
||||||
{#if Object.keys(filters).length > 0}
|
{#if filters.length > 0}
|
||||||
<div class="filterSummaryWrapper">
|
<div class="filterSummaryWrapper">
|
||||||
<FilterSummary {filters} onDeleteFilter={handleFilterDeleted} />
|
<FilterSummary
|
||||||
|
{filters}
|
||||||
|
{issues}
|
||||||
|
defaultStatuses={statuses}
|
||||||
|
onAddFilter={handleFilterMenuOpened}
|
||||||
|
onUpdateFilter={handleFiltersModified}
|
||||||
|
onDeleteFilter={handleFilterDeleted}
|
||||||
|
onChangeMode={handleFilterModeChanged}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<IssuesListBrowser
|
<IssuesListBrowser
|
||||||
|
@ -17,24 +17,35 @@
|
|||||||
import { Issue, IssueStatus } from '@anticrm/tracker'
|
import { Issue, IssueStatus } from '@anticrm/tracker'
|
||||||
import { showPopup } from '@anticrm/ui'
|
import { showPopup } from '@anticrm/ui'
|
||||||
import StatusFilterMenuSection from './StatusFilterMenuSection.svelte'
|
import StatusFilterMenuSection from './StatusFilterMenuSection.svelte'
|
||||||
|
import PriorityFilterMenuSection from './PriorityFilterMenuSection.svelte'
|
||||||
import FilterMenu from '../FilterMenu.svelte'
|
import FilterMenu from '../FilterMenu.svelte'
|
||||||
import { FilterAction, getGroupedIssues, getIssueFilterAssetsByType } from '../../utils'
|
import {
|
||||||
|
defaultPriorities,
|
||||||
|
FilterAction,
|
||||||
|
getGroupedIssues,
|
||||||
|
getIssueFilterAssetsByType,
|
||||||
|
IssueFilter
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
export let targetHtml: HTMLElement
|
export let targetHtml: HTMLElement
|
||||||
export let currentFilter: { [p: string]: { $in: any[] } } = {}
|
export let filters: IssueFilter[] = []
|
||||||
|
export let index: number = 0
|
||||||
export let defaultStatuses: Array<WithLookup<IssueStatus>> = []
|
export let defaultStatuses: Array<WithLookup<IssueStatus>> = []
|
||||||
export let issues: Issue[] = []
|
export let issues: Issue[] = []
|
||||||
export let onUpdate: (result: { [p: string]: any }) => void
|
export let onUpdate: (result: { [p: string]: any }, filterIndex: number) => void
|
||||||
export let onBack: () => void
|
export let onBack: (() => void) | undefined = undefined
|
||||||
|
|
||||||
|
$: currentFilterQuery = filters[index]?.query as { [p: string]: { $in?: any[]; $nin?: any[] } } | undefined
|
||||||
|
$: currentFilterMode = filters[index]?.mode
|
||||||
$: defaultStatusIds = defaultStatuses.map((x) => x._id)
|
$: defaultStatusIds = defaultStatuses.map((x) => x._id)
|
||||||
$: groupedByStatus = getGroupedIssues('status', issues, defaultStatusIds)
|
$: groupedByStatus = getGroupedIssues('status', issues, defaultStatusIds)
|
||||||
|
$: groupedByPriority = getGroupedIssues('priority', issues, defaultPriorities)
|
||||||
|
|
||||||
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||||
const statusGroups: { [key: string]: number } = {}
|
const statusGroups: { [key: string]: number } = {}
|
||||||
|
|
||||||
for (const defaultStatus of defaultStatuses) {
|
for (const status of defaultStatuses) {
|
||||||
statusGroups[defaultStatus._id] = groupedByStatus[defaultStatus._id]?.length ?? 0
|
statusGroups[status._id] = groupedByStatus[status._id]?.length ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
showPopup(
|
showPopup(
|
||||||
@ -42,7 +53,28 @@
|
|||||||
{
|
{
|
||||||
groups: statusGroups,
|
groups: statusGroups,
|
||||||
statuses: defaultStatuses,
|
statuses: defaultStatuses,
|
||||||
selectedElements: currentFilter.status?.$in,
|
selectedElements: currentFilterQuery?.status?.[currentFilterMode] ?? [],
|
||||||
|
index,
|
||||||
|
onUpdate,
|
||||||
|
onBack
|
||||||
|
},
|
||||||
|
targetHtml
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePriorityFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||||
|
const priorityGroups: { [key: string]: number } = {}
|
||||||
|
|
||||||
|
for (const priority of defaultPriorities) {
|
||||||
|
priorityGroups[priority] = groupedByPriority[priority]?.length ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
showPopup(
|
||||||
|
PriorityFilterMenuSection,
|
||||||
|
{
|
||||||
|
groups: priorityGroups,
|
||||||
|
selectedElements: currentFilterQuery?.priority?.[currentFilterMode] ?? [],
|
||||||
|
index,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onBack
|
onBack
|
||||||
},
|
},
|
||||||
@ -54,6 +86,10 @@
|
|||||||
{
|
{
|
||||||
...getIssueFilterAssetsByType('status'),
|
...getIssueFilterAssetsByType('status'),
|
||||||
onSelect: handleStatusFilterMenuSectionOpened
|
onSelect: handleStatusFilterMenuSectionOpened
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...getIssueFilterAssetsByType('priority'),
|
||||||
|
onSelect: handlePriorityFilterMenuSectionOpened
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { translate } from '@anticrm/platform'
|
||||||
|
import { IssuePriority } from '@anticrm/tracker'
|
||||||
|
import { IconNavPrev } from '@anticrm/ui'
|
||||||
|
import FilterMenuSection from '../FilterMenuSection.svelte'
|
||||||
|
import tracker from '../../plugin'
|
||||||
|
import { FilterSectionElement, issuePriorities } from '../../utils'
|
||||||
|
|
||||||
|
export let selectedElements: any[] = []
|
||||||
|
export let groups: { [key: string]: number }
|
||||||
|
export let index: number = 0
|
||||||
|
export let onUpdate: (result: { [p: string]: any }, filterIndex?: number) => void
|
||||||
|
export let onBack: (() => void) | undefined = undefined
|
||||||
|
|
||||||
|
const getFilterElements = async (groups: { [key: string]: number }, selected: any[]) => {
|
||||||
|
const elements: FilterSectionElement[] = onBack
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: IconNavPrev,
|
||||||
|
title: await translate(tracker.string.Back, {}),
|
||||||
|
onSelect: onBack
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(groups)) {
|
||||||
|
const priority = Number(key) as IssuePriority
|
||||||
|
const assets = issuePriorities[priority]
|
||||||
|
|
||||||
|
if (!assets) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push({
|
||||||
|
icon: assets.icon,
|
||||||
|
title: await translate(assets.label, {}),
|
||||||
|
count: value,
|
||||||
|
isSelected: selected.includes(priority),
|
||||||
|
onSelect: () => onUpdate({ priority }, index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await getFilterElements(groups, selectedElements) then actions}
|
||||||
|
<FilterMenuSection {actions} {onBack} on:close />
|
||||||
|
{/await}
|
@ -24,8 +24,9 @@
|
|||||||
export let selectedElements: any[] = []
|
export let selectedElements: any[] = []
|
||||||
export let statuses: Array<WithLookup<IssueStatus>> = []
|
export let statuses: Array<WithLookup<IssueStatus>> = []
|
||||||
export let groups: { [key: string]: number }
|
export let groups: { [key: string]: number }
|
||||||
export let onUpdate: (result: { [p: string]: any }) => void
|
export let index: number = 0
|
||||||
export let onBack: () => void
|
export let onUpdate: (result: { [p: string]: any }, filterIndex?: number) => void
|
||||||
|
export let onBack: (() => void) | undefined = undefined
|
||||||
|
|
||||||
let backButtonTitle = ''
|
let backButtonTitle = ''
|
||||||
|
|
||||||
@ -41,13 +42,15 @@
|
|||||||
selected: any[],
|
selected: any[],
|
||||||
backButtonTitle: string
|
backButtonTitle: string
|
||||||
) => {
|
) => {
|
||||||
const elements: FilterSectionElement[] = [
|
const elements: FilterSectionElement[] = onBack
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
icon: IconNavPrev,
|
icon: IconNavPrev,
|
||||||
title: backButtonTitle,
|
title: backButtonTitle,
|
||||||
onSelect: onBack
|
onSelect: onBack
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
: []
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(groups)) {
|
for (const [key, value] of Object.entries(groups)) {
|
||||||
const status = defaultStatuses.find((x) => x._id === key)
|
const status = defaultStatuses.find((x) => x._id === key)
|
||||||
@ -61,7 +64,7 @@
|
|||||||
title: status.name,
|
title: status.name,
|
||||||
count: value,
|
count: value,
|
||||||
isSelected: selected.includes(key),
|
isSelected: selected.includes(key),
|
||||||
onSelect: () => onUpdate({ status: key })
|
onSelect: () => onUpdate({ status: key }, index)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Ref, SortingOrder } from '@anticrm/core'
|
import { DocumentQuery, Ref, SortingOrder } from '@anticrm/core'
|
||||||
import type { Asset, IntlString } from '@anticrm/platform'
|
import type { Asset, IntlString } from '@anticrm/platform'
|
||||||
import {
|
import {
|
||||||
IssuePriority,
|
IssuePriority,
|
||||||
@ -147,6 +147,11 @@ export interface FilterSectionElement extends Omit<FilterAction, 'label'> {
|
|||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IssueFilter {
|
||||||
|
mode: '$in' | '$nin'
|
||||||
|
query: DocumentQuery<Issue>
|
||||||
|
}
|
||||||
|
|
||||||
export const getGroupedIssues = (
|
export const getGroupedIssues = (
|
||||||
key: IssuesGroupByKeys | undefined,
|
key: IssuesGroupByKeys | undefined,
|
||||||
elements: Issue[],
|
elements: Issue[],
|
||||||
@ -186,8 +191,40 @@ export const getIssueFilterAssetsByType = (type: string): { icon: Asset, label:
|
|||||||
label: tracker.string.Status
|
label: tracker.string.Status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'priority': {
|
||||||
|
return {
|
||||||
|
icon: tracker.icon.PriorityHigh,
|
||||||
|
label: tracker.string.Priority
|
||||||
|
}
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultPriorities = [
|
||||||
|
IssuePriority.NoPriority,
|
||||||
|
IssuePriority.Urgent,
|
||||||
|
IssuePriority.High,
|
||||||
|
IssuePriority.Medium,
|
||||||
|
IssuePriority.Low
|
||||||
|
]
|
||||||
|
|
||||||
|
export const getArraysIntersection = (a: any[], b: any[]): any[] => {
|
||||||
|
const setB = new Set(b)
|
||||||
|
const intersection = new Set(a.filter((x) => setB.has(x)))
|
||||||
|
|
||||||
|
return Array.from(intersection)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getArraysUnion = (a: any[], b: any[]): any[] => {
|
||||||
|
const setB = new Set(b)
|
||||||
|
const union = new Set(a)
|
||||||
|
|
||||||
|
for (const element of setB) {
|
||||||
|
union.add(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(union)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user