From 26052dca3ea081e3ed83e1ab1978b85fcbcc97e0 Mon Sep 17 00:00:00 2001
From: Andrey Sobolev <haiodo@users.noreply.github.com>
Date: Thu, 23 Mar 2023 12:41:27 +0700
Subject: [PATCH] TSK-924: Follow proper order for Tracker Kanban (#2815)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
---
 models/tracker/src/index.ts                   |  4 +--
 models/tracker/src/plugin.ts                  |  6 ++--
 .../src/components/issues/KanbanView.svelte   | 13 ++++---
 plugins/tracker-resources/src/plugin.ts       |  6 ++--
 plugins/tracker-resources/src/utils.ts        | 35 +++++++++++++++----
 .../src/components/list/List.svelte           |  2 +-
 .../src/components/list/ListCategories.svelte |  2 +-
 plugins/view-resources/src/utils.ts           | 21 ++++++++---
 plugins/view/src/index.ts                     |  8 ++---
 9 files changed, 68 insertions(+), 29 deletions(-)

diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts
index f7ad3320a3..e203d8b7c1 100644
--- a/models/tracker/src/index.ts
+++ b/models/tracker/src/index.ts
@@ -523,7 +523,7 @@ export function createModel (builder: Builder): void {
         key: 'shouldShowSubIssues',
         type: 'toggle',
         defaultValue: false,
-        actionTartget: 'query',
+        actionTarget: 'query',
         action: tracker.function.SubIssueQuery,
         label: tracker.string.SubIssues
       },
@@ -531,7 +531,7 @@ export function createModel (builder: Builder): void {
         key: 'shouldShowAll',
         type: 'toggle',
         defaultValue: false,
-        actionTartget: 'category',
+        actionTarget: 'category',
         action: view.function.ShowEmptyGroups,
         label: view.string.ShowEmptyGroups
       }
diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts
index 1a7d5a6754..f1ca65f082 100644
--- a/models/tracker/src/plugin.ts
+++ b/models/tracker/src/plugin.ts
@@ -20,7 +20,7 @@ import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
 import { trackerId } from '@hcengineering/tracker'
 import tracker from '@hcengineering/tracker-resources/src/plugin'
 import type { AnyComponent } from '@hcengineering/ui/src/types'
-import { Action, ViewAction, Viewlet, ViewletDescriptor } from '@hcengineering/view'
+import { Action, ViewAction, Viewlet } from '@hcengineering/view'
 import { Application } from '@hcengineering/workbench'
 
 export default mergeIds(trackerId, tracker, {
@@ -51,9 +51,7 @@ export default mergeIds(trackerId, tracker, {
     IssueList: '' as Ref<Viewlet>,
     IssueTemplateList: '' as Ref<Viewlet>,
     IssueKanban: '' as Ref<Viewlet>,
-    SprintList: '' as Ref<Viewlet>,
-    List: '' as Ref<ViewletDescriptor>,
-    Kanban: '' as Ref<ViewletDescriptor>
+    SprintList: '' as Ref<Viewlet>
   },
   completion: {
     IssueQuery: '' as Resource<ObjectSearchFactory>,
diff --git a/plugins/tracker-resources/src/components/issues/KanbanView.svelte b/plugins/tracker-resources/src/components/issues/KanbanView.svelte
index bd3c204fa6..872cadb15a 100644
--- a/plugins/tracker-resources/src/components/issues/KanbanView.svelte
+++ b/plugins/tracker-resources/src/components/issues/KanbanView.svelte
@@ -41,7 +41,7 @@
     showPopup,
     tooltip
   } from '@hcengineering/ui'
-  import { CategoryOption, ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
+  import { CategoryOption, Viewlet, ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
   import {
     ActionContext,
     focusStore,
@@ -52,6 +52,7 @@
     SelectDirection,
     selectionStore
   } from '@hcengineering/view-resources'
+  import { sortCategories } from '@hcengineering/view-resources/src/utils'
   import { onMount } from 'svelte'
   import tracker from '../../plugin'
   import { issuesGroupBySorting, mapKanbanCategories } from '../../utils'
@@ -71,6 +72,7 @@
   export let query: DocumentQuery<Issue> = {}
   export let viewOptionsConfig: ViewOptionModel[] | undefined
   export let viewOptions: ViewOptions
+  export let viewlet: Viewlet
 
   $: currentSpace = space || tracker.project.DefaultProject
   $: groupBy = (viewOptions.groupBy[0] ?? noCategory) as IssuesGrouping
@@ -99,7 +101,7 @@
     if (viewOptions === undefined) return query
     let result = hierarchy.clone(query)
     for (const viewOption of viewOptions) {
-      if (viewOption.actionTartget !== 'query') continue
+      if (viewOption.actionTarget !== 'query') continue
       const queryOption = viewOption as ViewQueryOption
       const f = await getResource(queryOption.action)
       result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
@@ -234,9 +236,9 @@
     sprints: Sprint[],
     assignee: Employee[]
   ) {
-    let categories = await getCategories(client, _class, docs, groupByKey)
+    let categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor)
     for (const viewOption of viewOptionsModel ?? []) {
-      if (viewOption.actionTartget !== 'category') continue
+      if (viewOption.actionTarget !== 'category') continue
       const categoryFunc = viewOption as CategoryOption
       if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
         const f = await getResource(categoryFunc.action)
@@ -247,7 +249,8 @@
               res.push(category)
             }
           }
-          categories = res
+
+          categories = await sortCategories(client, _class, res, groupByKey, viewlet.descriptor)
           break
         }
       }
diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts
index 1192cd6d8b..9ddd72d27e 100644
--- a/plugins/tracker-resources/src/plugin.ts
+++ b/plugins/tracker-resources/src/plugin.ts
@@ -17,12 +17,14 @@ import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
 import { mergeIds } from '@hcengineering/platform'
 import { IssueDraft } from '@hcengineering/tracker'
 import { AnyComponent, Location } from '@hcengineering/ui'
-import { SortFunc, Viewlet, ViewQueryAction } from '@hcengineering/view'
+import { SortFunc, Viewlet, ViewletDescriptor, ViewQueryAction } from '@hcengineering/view'
 import tracker, { trackerId } from '../../tracker/lib'
 
 export default mergeIds(trackerId, tracker, {
   viewlet: {
-    SubIssues: '' as Ref<Viewlet>
+    SubIssues: '' as Ref<Viewlet>,
+    List: '' as Ref<ViewletDescriptor>,
+    Kanban: '' as Ref<ViewletDescriptor>
   },
   string: {
     More: '' as IntlString,
diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts
index 1abdd5a633..ae4dd5b4b2 100644
--- a/plugins/tracker-resources/src/utils.ts
+++ b/plugins/tracker-resources/src/utils.ts
@@ -33,6 +33,8 @@ import { TypeState } from '@hcengineering/kanban'
 import { Asset, IntlString, translate } from '@hcengineering/platform'
 import { createQuery, getClient } from '@hcengineering/presentation'
 import {
+  Component,
+  ComponentStatus,
   Issue,
   IssuePriority,
   IssuesDateModificationPeriod,
@@ -40,11 +42,9 @@ import {
   IssuesOrdering,
   IssueStatus,
   IssueTemplateData,
-  Component,
-  ComponentStatus,
+  Project,
   Sprint,
   SprintStatus,
-  Project,
   TimeReportDayType
 } from '@hcengineering/tracker'
 import {
@@ -55,10 +55,11 @@ import {
   isWeekend,
   MILLISECONDS_IN_WEEK
 } from '@hcengineering/ui'
+import { ViewletDescriptor } from '@hcengineering/view'
 import { CategoryQuery, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
 import { writable } from 'svelte/store'
 import tracker from './plugin'
-import { defaultPriorities, defaultComponentStatuses, defaultSprintStatuses, issuePriorities } from './types'
+import { defaultComponentStatuses, defaultPriorities, defaultSprintStatuses, issuePriorities } from './types'
 
 export * from './types'
 
@@ -331,11 +332,33 @@ const listIssueStatusOrder = [
   tracker.issueStatusCategory.Canceled
 ] as const
 
-export async function issueStatusSort (value: Array<Ref<IssueStatus>>): Promise<Array<Ref<IssueStatus>>> {
+const listIssueKanbanStatusOrder = [
+  tracker.issueStatusCategory.Backlog,
+  tracker.issueStatusCategory.Unstarted,
+  tracker.issueStatusCategory.Started,
+  tracker.issueStatusCategory.Completed,
+  tracker.issueStatusCategory.Canceled
+] as const
+
+export async function issueStatusSort (
+  value: Array<Ref<IssueStatus>>,
+  viewletDescriptorId?: Ref<ViewletDescriptor>
+): Promise<Array<Ref<IssueStatus>>> {
   return await new Promise((resolve) => {
+    // TODO: How we track category updates.
     const query = createQuery(true)
     query.query(tracker.class.IssueStatus, { _id: { $in: value } }, (res) => {
-      res.sort((a, b) => listIssueStatusOrder.indexOf(a.category) - listIssueStatusOrder.indexOf(b.category))
+      if (viewletDescriptorId === tracker.viewlet.Kanban) {
+        res.sort((a, b) => {
+          const res = listIssueKanbanStatusOrder.indexOf(a.category) - listIssueKanbanStatusOrder.indexOf(b.category)
+          if (res === 0) {
+            return a.rank.localeCompare(b.rank)
+          }
+          return res
+        })
+      } else {
+        res.sort((a, b) => listIssueStatusOrder.indexOf(a.category) - listIssueStatusOrder.indexOf(b.category))
+      }
       resolve(res.map((p) => p._id))
       query.unsubscribe()
     })
diff --git a/plugins/view-resources/src/components/list/List.svelte b/plugins/view-resources/src/components/list/List.svelte
index 68e5dc699c..cd617fd654 100644
--- a/plugins/view-resources/src/components/list/List.svelte
+++ b/plugins/view-resources/src/components/list/List.svelte
@@ -86,7 +86,7 @@
     if (viewOptions === undefined) return query
     let result = hierarchy.clone(query)
     for (const viewOption of viewOptions) {
-      if (viewOption.actionTartget !== 'query') continue
+      if (viewOption.actionTarget !== 'query') continue
       const queryOption = viewOption as ViewQueryOption
       const f = await getResource(queryOption.action)
       result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
diff --git a/plugins/view-resources/src/components/list/ListCategories.svelte b/plugins/view-resources/src/components/list/ListCategories.svelte
index 0e9098d2fc..9576b70ed3 100644
--- a/plugins/view-resources/src/components/list/ListCategories.svelte
+++ b/plugins/view-resources/src/components/list/ListCategories.svelte
@@ -74,7 +74,7 @@
     categories = await getCategories(client, _class, docs, groupByKey)
     if (level === 0) {
       for (const viewOption of viewOptionsModel ?? []) {
-        if (viewOption.actionTartget !== 'category') continue
+        if (viewOption.actionTarget !== 'category') continue
         const categoryFunc = viewOption as CategoryOption
         if (viewOptions[viewOption.key] ?? viewOption.defaultValue) {
           const f = await getResource(categoryFunc.action)
diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts
index 1bc62a8398..5721d41d28 100644
--- a/plugins/view-resources/src/utils.ts
+++ b/plugins/view-resources/src/utils.ts
@@ -43,7 +43,7 @@ import {
   Location,
   locationToUrl
 } from '@hcengineering/ui'
-import type { BuildModelOptions, Viewlet } from '@hcengineering/view'
+import type { BuildModelOptions, Viewlet, ViewletDescriptor } from '@hcengineering/view'
 import view, { AttributeModel, BuildModelKey } from '@hcengineering/view'
 import { writable } from 'svelte/store'
 import plugin from './plugin'
@@ -528,11 +528,24 @@ export async function getCategories (
   client: TxOperations,
   _class: Ref<Class<Doc>>,
   docs: Doc[],
-  key: string
+  key: string,
+  viewletDescriptorId?: Ref<ViewletDescriptor>
+): Promise<any[]> {
+  if (key === noCategory) return [undefined]
+  const existingCategories = Array.from(new Set(docs.map((x: any) => x[key] ?? undefined)))
+
+  return await sortCategories(client, _class, existingCategories, key, viewletDescriptorId)
+}
+
+export async function sortCategories (
+  client: TxOperations,
+  _class: Ref<Class<Doc>>,
+  existingCategories: any[],
+  key: string,
+  viewletDescriptorId?: Ref<ViewletDescriptor>
 ): Promise<any[]> {
   if (key === noCategory) return [undefined]
   const hierarchy = client.getHierarchy()
-  const existingCategories = Array.from(new Set(docs.map((x: any) => x[key] ?? undefined)))
   const attr = hierarchy.getAttribute(_class, key)
   if (attr === undefined) return existingCategories
   const attrClass = getAttributePresenterClass(hierarchy, attr).attrClass
@@ -541,7 +554,7 @@ export async function getCategories (
   if (sortFunc?.func === undefined) return existingCategories
   const f = await getResource(sortFunc.func)
 
-  return await f(existingCategories)
+  return await f(existingCategories, viewletDescriptorId)
 }
 
 export function getKeyLabel<T extends Doc> (
diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts
index 5981597631..be880e56a9 100644
--- a/plugins/view/src/index.ts
+++ b/plugins/view/src/index.ts
@@ -236,7 +236,7 @@ export interface ListHeaderExtra extends Class<Doc> {
 /**
  * @public
  */
-export type SortFunc = Resource<(values: any[]) => Promise<any[]>>
+export type SortFunc = Resource<(values: any[], viewletDescriptorId?: Ref<ViewletDescriptor>) => Promise<any[]>>
 
 /**
  * @public
@@ -483,7 +483,7 @@ export interface ViewOption {
   defaultValue: any
   label: IntlString
   hidden?: (viewOptions: ViewOptions) => boolean
-  actionTartget?: 'query' | 'category'
+  actionTarget?: 'query' | 'category'
   action?: Resource<(value: any, ...params: any) => any>
 }
 
@@ -504,7 +504,7 @@ export type ViewCategoryAction = Resource<
  * @public
  */
 export interface CategoryOption extends ViewOption {
-  actionTartget: 'category'
+  actionTarget: 'category'
   action: ViewCategoryAction
 }
 
@@ -517,7 +517,7 @@ export type ViewQueryAction = Resource<(value: any, query: DocumentQuery<Doc>) =
  * @public
  */
 export interface ViewQueryOption extends ViewOption {
-  actionTartget: 'query'
+  actionTarget: 'query'
   action: ViewQueryAction
 }