From 1c98ca3c3a03a0e55b28d5b8bd57d54caf3566f7 Mon Sep 17 00:00:00 2001 From: Artyom Grigorovich <grigorovichartyom@gmail.com> Date: Thu, 12 May 2022 00:15:29 +0700 Subject: [PATCH] Tracker: Issue filters - additional features (#1708) Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com> --- .../src/components/FilterMenuSection.svelte | 17 +-- .../src/components/FilterSummary.svelte | 95 ++++++++++++-- .../components/FilterSummarySection.svelte | 15 ++- .../src/components/PrioritySelector.svelte | 10 +- .../src/components/issues/Issues.svelte | 118 ++++++++++++------ .../components/issues/IssuesFilterMenu.svelte | 50 ++++++-- .../issues/PriorityFilterMenuSection.svelte | 63 ++++++++++ .../issues/StatusFilterMenuSection.svelte | 23 ++-- plugins/tracker-resources/src/utils.ts | 39 +++++- 9 files changed, 350 insertions(+), 80 deletions(-) create mode 100644 plugins/tracker-resources/src/components/issues/PriorityFilterMenuSection.svelte diff --git a/plugins/tracker-resources/src/components/FilterMenuSection.svelte b/plugins/tracker-resources/src/components/FilterMenuSection.svelte index be449f1532..623ad0081b 100644 --- a/plugins/tracker-resources/src/components/FilterMenuSection.svelte +++ b/plugins/tracker-resources/src/components/FilterMenuSection.svelte @@ -18,7 +18,7 @@ import { FilterSectionElement } from '../utils' export let actions: FilterSectionElement[] = [] - export let onBack: () => void + export let onBack: (() => void) | undefined = undefined const dispatch = createEventDispatcher() const actionElements: HTMLButtonElement[] = [] @@ -28,7 +28,7 @@ const getSelectedElementsMap = (actions: FilterSectionElement[]) => { 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 } @@ -46,7 +46,8 @@ if (event.key === 'ArrowLeft') { dispatch('close') - onBack() + + onBack?.() } } @@ -74,25 +75,27 @@ event.currentTarget.focus() }} on:click={(event) => { - if (i === 0) { + if (i === 0 && onBack) { dispatch('close') } action.onSelect(event) - if (i !== 0) { + if (i !== 0 || !onBack) { selectedElementsMap[i] = !selectedElementsMap[i] } }} > <div class="buttonContent"> - {#if i !== 0} + {#if i !== 0 || (i === 0 && !onBack)} <div class="flex check pointer-events-none"> <CheckBox checked={selectedElementsMap[i]} primary /> </div> {/if} {#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 action.title} <div class="ml-3 pr-1">{action.title}</div> diff --git a/plugins/tracker-resources/src/components/FilterSummary.svelte b/plugins/tracker-resources/src/components/FilterSummary.svelte index 92ee7ad69d..23a6c84742 100644 --- a/plugins/tracker-resources/src/components/FilterSummary.svelte +++ b/plugins/tracker-resources/src/components/FilterSummary.svelte @@ -13,26 +13,103 @@ // limitations under the License. --> <script lang="ts"> - import { Button, IconAdd } from '@anticrm/ui' - + import { Button, eventToHTMLElement, IconAdd, showPopup } from '@anticrm/ui' 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 onDeleteFilter: (filterKey?: string) => void + export let filters: IssueFilter[] = [] + 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> <div class="root"> - {#each Object.entries(filters) as [key, value]} - <FilterSummarySection type={key} selectedFilters={value} onDelete={() => onDeleteFilter(key)} /> + {#each filters as filter, filterIndex} + {@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} - <div class="ml-2"> - <Button kind={'link'} icon={IconAdd} /> - </div> + {#if onAddFilter} + <div class="ml-2"> + <Button kind={'link'} icon={IconAdd} on:click={onAddFilter} /> + </div> + {/if} </div> <style lang="scss"> .root { display: flex; + flex: 1 1 auto; + flex-flow: row wrap; align-items: center; } </style> diff --git a/plugins/tracker-resources/src/components/FilterSummarySection.svelte b/plugins/tracker-resources/src/components/FilterSummarySection.svelte index 34d31194bd..088a5d3253 100644 --- a/plugins/tracker-resources/src/components/FilterSummarySection.svelte +++ b/plugins/tracker-resources/src/components/FilterSummarySection.svelte @@ -19,8 +19,11 @@ import tracker from '../plugin' export let type: string = '' + export let mode: '$in' | '$nin' = '$in' export let selectedFilters: any[] = [] export let onDelete: () => void + export let onChangeMode: () => void + export let onEditFilter: (event: MouseEvent) => void </script> <div class="root"> @@ -30,7 +33,12 @@ <div class="buttonWrapper"> <Button 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 class="buttonWrapper"> @@ -38,6 +46,7 @@ shape={'rectangle'} label={tracker.string.FilterStatesCount} labelParams={{ value: selectedFilters.length }} + on:click={onEditFilter} /> </div> <div class="buttonWrapper"> @@ -49,6 +58,10 @@ .root { display: flex; align-items: center; + + &:not(:first-child) { + margin-left: 0.5rem; + } } .buttonWrapper { diff --git a/plugins/tracker-resources/src/components/PrioritySelector.svelte b/plugins/tracker-resources/src/components/PrioritySelector.svelte index b48173384e..166a4f39d8 100644 --- a/plugins/tracker-resources/src/components/PrioritySelector.svelte +++ b/plugins/tracker-resources/src/components/PrioritySelector.svelte @@ -16,7 +16,7 @@ import { IssuePriority } from '@anticrm/tracker' import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui' import type { ButtonKind, ButtonSize } from '@anticrm/ui' - import { issuePriorities } from '../utils' + import { defaultPriorities, issuePriorities } from '../utils' import tracker from '../plugin' export let priority: IssuePriority @@ -29,13 +29,7 @@ export let justify: 'left' | 'center' = 'center' export let width: string | undefined = 'min-content' - const prioritiesInfo = [ - IssuePriority.NoPriority, - IssuePriority.Urgent, - IssuePriority.High, - IssuePriority.Medium, - IssuePriority.Low - ].map((p) => ({ id: p, ...issuePriorities[p] })) + const prioritiesInfo = defaultPriorities.map((p) => ({ id: p, ...issuePriorities[p] })) const handlePriorityEditorOpened = (event: MouseEvent) => { if (!isEditable) { diff --git a/plugins/tracker-resources/src/components/issues/Issues.svelte b/plugins/tracker-resources/src/components/issues/Issues.svelte index c583a5620e..3b8f8bf1b8 100644 --- a/plugins/tracker-resources/src/components/issues/Issues.svelte +++ b/plugins/tracker-resources/src/components/issues/Issues.svelte @@ -23,8 +23,7 @@ IssuesOrdering, IssuesDateModificationPeriod, IssueStatus, - IssueStatusCategory, - IssuePriority + IssueStatusCategory } from '@anticrm/tracker' import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement, IconAdd, IconClose } from '@anticrm/ui' import { IntlString } from '@anticrm/platform' @@ -40,7 +39,11 @@ issuesOrderKeyMap, getIssuesModificationDatePeriodTime, issuesSortOrderMap, - getGroupedIssues + getGroupedIssues, + defaultPriorities, + getArraysIntersection, + IssueFilter, + getArraysUnion } from '../../utils' export let currentSpace: Ref<Team> @@ -62,7 +65,7 @@ const issuesMap: { [status: string]: number } = {} let filterElement: HTMLElement | null = null - let filters: { [p: string]: any[] } = {} + let filters: IssueFilter[] = [] let currentTeam: Team | undefined let issues: Issue[] = [] let resultIssues: Issue[] = [] @@ -71,7 +74,7 @@ $: totalIssuesCount = issues.length $: resultIssuesCount = resultIssues.length - $: isFiltersEmpty = Object.keys(filters).length === 0 + $: isFiltersEmpty = filters.length === 0 const options: FindOptions<Issue> = { sort: { [issuesOrderKeyMap[orderingKey]]: issuesSortOrderMap[issuesOrderKeyMap[orderingKey]] }, @@ -189,13 +192,6 @@ 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( @@ -318,66 +314,104 @@ ) } - const getFiltersQuery = (filterParams: { [f: string]: any[] }) => { - const result: { [f: string]: { $in: any[] } } = {} + const getFiltersQuery = (filters: IssueFilter[]) => { + const result: { [f: string]: { $in?: any[]; $nin?: any[] } } = {} - for (const [key, value] of Object.entries(filterParams)) { - result[key] = { $in: [...value] } + for (const filter of filters) { + 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 } - const handleFilterDeleted = (filterKey?: string) => { - if (filterKey) { - delete filters[filterKey] - - filters = filters + const handleFilterDeleted = (filterIndex?: number) => { + if (filterIndex !== undefined) { + filters.splice(filterIndex, 1) } else { - filters = {} + filters.length = 0 } + + filters = filters } const handleAllFiltersDeleted = () => { 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) if (entries.length !== 1) { return } - const [filter, filterValue] = entries[0] + const [filterKey, filterValue] = entries[0] - if (filter in filters) { - if (filters[filter].includes(filterValue)) { - filters[filter] = filters[filter].filter((x) => x !== filterValue) + if (filters[i]) { + const { mode, query: currentFilterQuery } = filters[i] + const currentFilterQueryConditions: any[] = currentFilterQuery[filterKey]?.[mode] ?? [] - if (Object.keys(filters).length === 1 && filters[filter].length === 0) { - filters = {} + if (currentFilterQueryConditions.includes(filterValue)) { + 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 { - filters[filter] = [...filters[filter], filterValue] + filters[i] = { mode, query: { [filterKey]: { $in: [...currentFilterQueryConditions, filterValue] } } } } } 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) => { dispatch('close') - handleFilterMenuOpened(event) + handleFilterMenuOpened(event, false) } - const handleFilterMenuOpened = (event: MouseEvent) => { + const handleFilterMenuOpened = (event: MouseEvent, shouldUpdateFilterTargetElement: boolean = true) => { if (!currentSpace) { return } - if (filterElement === null) { + if (!filterElement || shouldUpdateFilterTargetElement) { filterElement = eventToHTMLElement(event) } @@ -385,7 +419,8 @@ IssuesFilterMenu, { issues, - currentFilter: getFiltersQuery(filters), + filters: filters, + index: filters.length, defaultStatuses: statuses, onBack: handleFiltersBackButtonPressed, targetHtml: filterElement, @@ -410,6 +445,7 @@ {/if} <div class="ml-3"> <Button + size="small" icon={isFiltersEmpty ? IconAdd : IconClose} kind={'link-bordered'} borderStyle={'dashed'} @@ -420,9 +456,17 @@ </div> <Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} /> </div> - {#if Object.keys(filters).length > 0} + {#if filters.length > 0} <div class="filterSummaryWrapper"> - <FilterSummary {filters} onDeleteFilter={handleFilterDeleted} /> + <FilterSummary + {filters} + {issues} + defaultStatuses={statuses} + onAddFilter={handleFilterMenuOpened} + onUpdateFilter={handleFiltersModified} + onDeleteFilter={handleFilterDeleted} + onChangeMode={handleFilterModeChanged} + /> </div> {/if} <IssuesListBrowser diff --git a/plugins/tracker-resources/src/components/issues/IssuesFilterMenu.svelte b/plugins/tracker-resources/src/components/issues/IssuesFilterMenu.svelte index 21799264c2..636e11239c 100644 --- a/plugins/tracker-resources/src/components/issues/IssuesFilterMenu.svelte +++ b/plugins/tracker-resources/src/components/issues/IssuesFilterMenu.svelte @@ -17,24 +17,35 @@ import { Issue, IssueStatus } from '@anticrm/tracker' import { showPopup } from '@anticrm/ui' import StatusFilterMenuSection from './StatusFilterMenuSection.svelte' + import PriorityFilterMenuSection from './PriorityFilterMenuSection.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 currentFilter: { [p: string]: { $in: any[] } } = {} + export let filters: IssueFilter[] = [] + export let index: number = 0 export let defaultStatuses: Array<WithLookup<IssueStatus>> = [] export let issues: Issue[] = [] - export let onUpdate: (result: { [p: string]: any }) => void - export let onBack: () => void + export let onUpdate: (result: { [p: string]: any }, filterIndex: number) => 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) $: groupedByStatus = getGroupedIssues('status', issues, defaultStatusIds) + $: groupedByPriority = getGroupedIssues('priority', issues, defaultPriorities) const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => { const statusGroups: { [key: string]: number } = {} - for (const defaultStatus of defaultStatuses) { - statusGroups[defaultStatus._id] = groupedByStatus[defaultStatus._id]?.length ?? 0 + for (const status of defaultStatuses) { + statusGroups[status._id] = groupedByStatus[status._id]?.length ?? 0 } showPopup( @@ -42,7 +53,28 @@ { groups: statusGroups, 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, onBack }, @@ -54,6 +86,10 @@ { ...getIssueFilterAssetsByType('status'), onSelect: handleStatusFilterMenuSectionOpened + }, + { + ...getIssueFilterAssetsByType('priority'), + onSelect: handlePriorityFilterMenuSectionOpened } ] </script> diff --git a/plugins/tracker-resources/src/components/issues/PriorityFilterMenuSection.svelte b/plugins/tracker-resources/src/components/issues/PriorityFilterMenuSection.svelte new file mode 100644 index 0000000000..22085d32f3 --- /dev/null +++ b/plugins/tracker-resources/src/components/issues/PriorityFilterMenuSection.svelte @@ -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} diff --git a/plugins/tracker-resources/src/components/issues/StatusFilterMenuSection.svelte b/plugins/tracker-resources/src/components/issues/StatusFilterMenuSection.svelte index 3ea98d0fa5..7d34dafca8 100644 --- a/plugins/tracker-resources/src/components/issues/StatusFilterMenuSection.svelte +++ b/plugins/tracker-resources/src/components/issues/StatusFilterMenuSection.svelte @@ -24,8 +24,9 @@ export let selectedElements: any[] = [] export let statuses: Array<WithLookup<IssueStatus>> = [] export let groups: { [key: string]: number } - export let onUpdate: (result: { [p: string]: any }) => void - export let onBack: () => void + export let index: number = 0 + export let onUpdate: (result: { [p: string]: any }, filterIndex?: number) => void + export let onBack: (() => void) | undefined = undefined let backButtonTitle = '' @@ -41,13 +42,15 @@ selected: any[], backButtonTitle: string ) => { - const elements: FilterSectionElement[] = [ - { - icon: IconNavPrev, - title: backButtonTitle, - onSelect: onBack - } - ] + const elements: FilterSectionElement[] = onBack + ? [ + { + icon: IconNavPrev, + title: backButtonTitle, + onSelect: onBack + } + ] + : [] for (const [key, value] of Object.entries(groups)) { const status = defaultStatuses.find((x) => x._id === key) @@ -61,7 +64,7 @@ title: status.name, count: value, isSelected: selected.includes(key), - onSelect: () => onUpdate({ status: key }) + onSelect: () => onUpdate({ status: key }, index) }) } diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts index f3dadb96dc..19d16278b3 100644 --- a/plugins/tracker-resources/src/utils.ts +++ b/plugins/tracker-resources/src/utils.ts @@ -13,7 +13,7 @@ // 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 { IssuePriority, @@ -147,6 +147,11 @@ export interface FilterSectionElement extends Omit<FilterAction, 'label'> { isSelected?: boolean } +export interface IssueFilter { + mode: '$in' | '$nin' + query: DocumentQuery<Issue> +} + export const getGroupedIssues = ( key: IssuesGroupByKeys | undefined, elements: Issue[], @@ -186,8 +191,40 @@ export const getIssueFilterAssetsByType = (type: string): { icon: Asset, label: label: tracker.string.Status } } + case 'priority': { + return { + icon: tracker.icon.PriorityHigh, + label: tracker.string.Priority + } + } default: { 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) +}