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