From 57a925b038940642d371a293a5096d64c640538d Mon Sep 17 00:00:00 2001
From: Andrey Sobolev <haiodo@users.noreply.github.com>
Date: Mon, 27 Mar 2023 22:33:23 +0700
Subject: [PATCH] TSK-955: Fix status display (#2840)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
---
 packages/kanban/src/utils.ts                  |  3 ++
 packages/presentation/src/utils.ts            | 36 +++++++++++++++++--
 packages/query/src/index.ts                   | 22 ++++++++++--
 plugins/client-resources/src/connection.ts    | 11 ++++--
 plugins/contact-resources/src/utils.ts        |  4 +--
 plugins/task/src/utils.ts                     |  4 ++-
 .../src/components/icons/StatusIcon.svelte    |  1 +
 .../issues/IssueStatusActivity.svelte         |  4 +--
 .../components/issues/IssueStatusIcon.svelte  |  2 +-
 .../src/components/issues/StatusEditor.svelte |  2 +-
 .../components/issues/StatusPresenter.svelte  |  4 +--
 .../issues/StatusRefPresenter.svelte          |  4 +--
 .../components/issues/TitlePresenter.svelte   | 18 +++++-----
 .../issues/edit/SubIssuesSelector.svelte      |  6 ++--
 .../related/RelatedIssueSelector.svelte       |  6 ++--
 .../components/sprints/IssueStatistics.svelte |  6 ++--
 .../src/components/workflow/Statuses.svelte   | 28 ++++++++-------
 plugins/tracker-resources/src/utils.ts        | 19 +++++++---
 plugins/tracker/src/utils.ts                  |  4 ++-
 plugins/workbench-resources/src/connect.ts    | 14 +++++++-
 tests/sanity/package.json                     |  4 +--
 tests/sanity/tests/tracker.spec.ts            |  4 +--
 22 files changed, 145 insertions(+), 61 deletions(-)

diff --git a/packages/kanban/src/utils.ts b/packages/kanban/src/utils.ts
index ae91f408bb..0961261f6e 100644
--- a/packages/kanban/src/utils.ts
+++ b/packages/kanban/src/utils.ts
@@ -40,5 +40,8 @@ export const calcRank = (prev?: { rank: string }, next?: { rank: string }): stri
   const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
   const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
 
+  if (a.equals(b)) {
+    return a.genNext().toString()
+  }
   return a.between(b).toString()
 }
diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts
index 35381225bf..e7dbec7130 100644
--- a/packages/presentation/src/utils.ts
+++ b/packages/presentation/src/utils.ts
@@ -104,6 +104,10 @@ export function getClient (): TxOperations {
  * @public
  */
 export function setClient (_client: Client): void {
+  if (liveQuery !== undefined) {
+    void liveQuery.close()
+  }
+  const needRefresh = liveQuery !== undefined
   liveQuery = new LQ(_client)
   client = new UIClient(_client, liveQuery)
   _client.notify = (tx: Tx) => {
@@ -111,17 +115,23 @@ export function setClient (_client: Client): void {
 
     txListeners.forEach((it) => it(tx))
   }
+  if (needRefresh) {
+    refreshClient()
+  }
 }
 
 /**
  * @public
  */
 export function refreshClient (): void {
-  if (liveQuery !== undefined) {
-    void liveQuery.refreshConnect()
+  void liveQuery?.refreshConnect()
+  for (const q of globalQueries) {
+    q.refreshClient()
   }
 }
 
+const globalQueries: LiveQuery[] = []
+
 /**
  * @public
  */
@@ -137,6 +147,8 @@ export class LiveQuery {
       onDestroy(() => {
         this.unsubscribe()
       })
+    } else {
+      globalQueries.push(this)
     }
   }
 
@@ -149,11 +161,21 @@ export class LiveQuery {
     if (!this.needUpdate(_class, query, callback, options)) {
       return false
     }
+    return this.doQuery<T>(_class, query, callback, options)
+  }
+
+  private doQuery<T extends Doc>(
+    _class: Ref<Class<T>>,
+    query: DocumentQuery<T>,
+    callback: (result: FindResult<T>) => void,
+    options: FindOptions<T> | undefined
+  ): boolean {
     this.unsubscribe()
     this.oldCallback = callback
     this.oldClass = _class
     this.oldOptions = options
     this.oldQuery = query
+
     const unsub = liveQuery.query(_class, query, callback, options)
     this.unsubscribe = () => {
       unsub()
@@ -166,6 +188,16 @@ export class LiveQuery {
     return true
   }
 
+  refreshClient (): void {
+    if (this.oldClass !== undefined && this.oldQuery !== undefined && this.oldCallback !== undefined) {
+      const _class = this.oldClass
+      const query = this.oldQuery
+      const callback = this.oldCallback
+      const options = this.oldOptions
+      this.doQuery(_class, query, callback, options)
+    }
+  }
+
   private needUpdate<T extends Doc>(
     _class: Ref<Class<T>>,
     query: DocumentQuery<T>,
diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts
index 3e927fa418..9afe9c002b 100644
--- a/packages/query/src/index.ts
+++ b/packages/query/src/index.ts
@@ -64,7 +64,7 @@ interface Query {
  * @public
  */
 export class LiveQuery extends TxProcessor implements Client {
-  private readonly client: Client
+  private client: Client
   private readonly queries: Map<Ref<Class<Doc>>, Query[]> = new Map<Ref<Class<Doc>>, Query[]>()
   private readonly queue: Query[] = []
 
@@ -73,6 +73,11 @@ export class LiveQuery extends TxProcessor implements Client {
     this.client = client
   }
 
+  async updateClient (client: Client): Promise<void> {
+    this.client = client
+    await this.refreshConnect()
+  }
+
   async close (): Promise<void> {
     return await this.client.close()
   }
@@ -89,7 +94,20 @@ export class LiveQuery extends TxProcessor implements Client {
   async refreshConnect (): Promise<void> {
     for (const q of [...this.queue]) {
       if (!(await this.removeFromQueue(q))) {
-        await this.refresh(q)
+        try {
+          await this.refresh(q)
+        } catch (err) {
+          console.error(err)
+        }
+      }
+    }
+    for (const v of this.queries.values()) {
+      for (const q of v) {
+        try {
+          await this.refresh(q)
+        } catch (err) {
+          console.error(err)
+        }
       }
     }
   }
diff --git a/plugins/client-resources/src/connection.ts b/plugins/client-resources/src/connection.ts
index 279ced9efd..ddd11fa0d2 100644
--- a/plugins/client-resources/src/connection.ts
+++ b/plugins/client-resources/src/connection.ts
@@ -50,7 +50,7 @@ class RequestPromise {
   resolve!: (value?: any) => void
   reject!: (reason?: any) => void
   reconnect?: () => void
-  constructor () {
+  constructor (readonly method: string, readonly params: any[]) {
     this.promise = new Promise((resolve, reject) => {
       this.resolve = resolve
       this.reject = reject
@@ -64,6 +64,7 @@ class Connection implements ClientConnection {
   private lastId = 0
   private readonly interval: number
   private readonly sessionId = generateId() as string
+  private closed = false
 
   constructor (
     private readonly url: string,
@@ -80,6 +81,7 @@ class Connection implements ClientConnection {
   }
 
   async close (): Promise<void> {
+    this.closed = true
     clearInterval(this.interval)
     if (this.websocket !== null) {
       if (this.websocket instanceof Promise) {
@@ -158,7 +160,7 @@ class Connection implements ClientConnection {
           }
           this.requests.delete(resp.id)
           if (resp.error !== undefined) {
-            console.log('ERROR', resp.id)
+            console.log('ERROR', promise, resp.id)
             promise.reject(new PlatformError(resp.error))
           } else {
             promise.resolve(resp.result)
@@ -212,8 +214,11 @@ class Connection implements ClientConnection {
     // If not defined, on reconnect with timeout, will retry automatically.
     retry?: () => Promise<boolean>
   }): Promise<any> {
+    if (this.closed) {
+      throw new PlatformError(unknownError('connection closed'))
+    }
     const id = this.lastId++
-    const promise = new RequestPromise()
+    const promise = new RequestPromise(data.method, data.params)
 
     const sendData = async (): Promise<void> => {
       if (this.websocket instanceof Promise) {
diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts
index dcc0bae994..5e9dab32e6 100644
--- a/plugins/contact-resources/src/utils.ts
+++ b/plugins/contact-resources/src/utils.ts
@@ -34,10 +34,8 @@ import { FilterQuery } from '@hcengineering/view-resources'
 import { get, writable } from 'svelte/store'
 import contact from './plugin'
 
-const client = getClient()
-
 export async function getChannelProviders (): Promise<Map<Ref<ChannelProvider>, ChannelProvider>> {
-  const cp = await client.findAll(contact.class.ChannelProvider, {})
+  const cp = await getClient().findAll(contact.class.ChannelProvider, {})
   const map = new Map<Ref<ChannelProvider>, ChannelProvider>()
   for (const provider of cp) {
     map.set(provider._id, provider)
diff --git a/plugins/task/src/utils.ts b/plugins/task/src/utils.ts
index ae91f408bb..f8b573e900 100644
--- a/plugins/task/src/utils.ts
+++ b/plugins/task/src/utils.ts
@@ -39,6 +39,8 @@ export const genRanks = (count: number): Generator<string, void, unknown> =>
 export const calcRank = (prev?: { rank: string }, next?: { rank: string }): string => {
   const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
   const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
-
+  if (a.equals(b)) {
+    return a.genNext().toString()
+  }
   return a.between(b).toString()
 }
diff --git a/plugins/tracker-resources/src/components/icons/StatusIcon.svelte b/plugins/tracker-resources/src/components/icons/StatusIcon.svelte
index eec51029c5..7a05b9fdcd 100644
--- a/plugins/tracker-resources/src/components/icons/StatusIcon.svelte
+++ b/plugins/tracker-resources/src/components/icons/StatusIcon.svelte
@@ -15,6 +15,7 @@
 <svg
   class="svg-{size}"
   {fill}
+  id={category._id}
   style:transform={category._id === tracker.issueStatusCategory.Started ? 'rotate(-90deg)' : ''}
   viewBox="0 0 14 14"
   xmlns="http://www.w3.org/2000/svg"
diff --git a/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte b/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte
index 02b8d1e67c..c55701ba04 100644
--- a/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte
+++ b/plugins/tracker-resources/src/components/issues/IssueStatusActivity.svelte
@@ -4,7 +4,7 @@
   import { Issue, IssueStatus } from '@hcengineering/tracker'
   import { Label, ticker } from '@hcengineering/ui'
   import tracker from '../../plugin'
-  import { statusByIdStore } from '../../utils'
+  import { statusStore } from '../../utils'
   import Duration from './Duration.svelte'
   import StatusPresenter from './StatusPresenter.svelte'
 
@@ -83,7 +83,7 @@
     displaySt = result
   }
 
-  $: updateStatus(txes, $statusByIdStore, $ticker)
+  $: updateStatus(txes, $statusStore.byId, $ticker)
 </script>
 
 <div class="flex-row mt-4 mb-4">
diff --git a/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte b/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte
index 62f858b444..2ce96483e2 100644
--- a/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte
+++ b/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte
@@ -40,7 +40,7 @@
 
   $: if (value.category === tracker.issueStatusCategory.Started) {
     const _s = [
-      ...$statusStore.filter(
+      ...$statusStore.statuses.filter(
         (it) => it.attachedTo === value.attachedTo && it.category === tracker.issueStatusCategory.Started
       )
     ]
diff --git a/plugins/tracker-resources/src/components/issues/StatusEditor.svelte b/plugins/tracker-resources/src/components/issues/StatusEditor.svelte
index 2da4231146..18be26864d 100644
--- a/plugins/tracker-resources/src/components/issues/StatusEditor.svelte
+++ b/plugins/tracker-resources/src/components/issues/StatusEditor.svelte
@@ -65,7 +65,7 @@
     )
   }
 
-  $: statuses = $statusStore.filter((it) => it.attachedTo === value?.space)
+  $: statuses = $statusStore.statuses.filter((it) => it.attachedTo === value?.space)
 
   $: selectedStatus = statuses?.find((status) => status._id === value.status) ?? statuses?.[0]
   $: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
diff --git a/plugins/tracker-resources/src/components/issues/StatusPresenter.svelte b/plugins/tracker-resources/src/components/issues/StatusPresenter.svelte
index 369ccf9c36..846159bfca 100644
--- a/plugins/tracker-resources/src/components/issues/StatusPresenter.svelte
+++ b/plugins/tracker-resources/src/components/issues/StatusPresenter.svelte
@@ -14,7 +14,7 @@
 -->
 <script lang="ts">
   import { IssueStatus } from '@hcengineering/tracker'
-  import { statusByIdStore } from '../../utils'
+  import { statusStore } from '../../utils'
   import IssueStatusIcon from './IssueStatusIcon.svelte'
 
   export let value: IssueStatus | undefined
@@ -22,7 +22,7 @@
 </script>
 
 {#if value}
-  {@const icon = $statusByIdStore.get(value._id)?.$lookup?.category?.icon}
+  {@const icon = $statusStore.byId.get(value._id)?.$lookup?.category?.icon}
   <div class="flex-presenter">
     {#if icon}
       <IssueStatusIcon {value} {size} />
diff --git a/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte b/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte
index a23248560a..2f98d49d8f 100644
--- a/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte
+++ b/plugins/tracker-resources/src/components/issues/StatusRefPresenter.svelte
@@ -15,7 +15,7 @@
 <script lang="ts">
   import { Ref } from '@hcengineering/core'
   import { IssueStatus } from '@hcengineering/tracker'
-  import { statusByIdStore } from '../../utils'
+  import { statusStore } from '../../utils'
   import StatusPresenter from './StatusPresenter.svelte'
 
   export let value: Ref<IssueStatus> | undefined
@@ -23,5 +23,5 @@
 </script>
 
 {#if value}
-  <StatusPresenter value={$statusByIdStore.get(value)} {size} />
+  <StatusPresenter value={$statusStore.byId.get(value)} {size} />
 {/if}
diff --git a/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte b/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte
index be1a409f5b..5bfaec4fb8 100644
--- a/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte
+++ b/plugins/tracker-resources/src/components/issues/TitlePresenter.svelte
@@ -26,16 +26,16 @@
 </script>
 
 {#if value}
-  <DocNavLink object={value} {onClick} component={tracker.component.EditIssue} inline shrink={1}>
-    <span
-      class="name overflow-label select-text"
-      class:with-margin={shouldUseMargin}
-      style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
-      title={value.title}
-    >
+  <span
+    class="name overflow-label select-text"
+    class:with-margin={shouldUseMargin}
+    style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
+    title={value.title}
+  >
+    <DocNavLink object={value} {onClick} component={tracker.component.EditIssue} inline shrink={1}>
       {value.title}
-    </span>
-  </DocNavLink>
+    </DocNavLink>
+  </span>
   {#if showParent}
     <ParentNamesPresenter {value} />
   {/if}
diff --git a/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte b/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte
index 64f1b651e8..c04d377dee 100644
--- a/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte
+++ b/plugins/tracker-resources/src/components/issues/edit/SubIssuesSelector.svelte
@@ -29,7 +29,7 @@
   } from '@hcengineering/ui'
   import { getIssueId } from '../../../issues'
   import tracker from '../../../plugin'
-  import { statusByIdStore, statusStore, subIssueListProvider } from '../../../utils'
+  import { statusStore, subIssueListProvider } from '../../../utils'
 
   export let value: WithLookup<Issue>
   export let currentProject: Project | undefined = undefined
@@ -77,7 +77,7 @@
   }
 
   $: if (subIssues) {
-    const doneStatuses = $statusStore
+    const doneStatuses = $statusStore.statuses
       .filter((s) => s.category === tracker.issueStatusCategory.Completed)
       .map((p) => p._id)
     countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
@@ -115,7 +115,7 @@
               id: iss._id,
               text,
               isSelected: iss._id === value._id,
-              ...getIssueStatusIcon(iss, $statusByIdStore)
+              ...getIssueStatusIcon(iss, $statusStore.byId)
             }
           }),
           width: 'large'
diff --git a/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte b/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte
index ce049b01c1..d8d2e48f7e 100644
--- a/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte
+++ b/plugins/tracker-resources/src/components/issues/related/RelatedIssueSelector.svelte
@@ -29,7 +29,7 @@
   } from '@hcengineering/ui'
   import { getIssueId } from '../../../issues'
   import tracker from '../../../plugin'
-  import { statusByIdStore, statusStore, subIssueListProvider } from '../../../utils'
+  import { statusStore, subIssueListProvider } from '../../../utils'
 
   export let object: WithLookup<Doc & { related: number }> | undefined
   export let value: WithLookup<Doc & { related: number }> | undefined
@@ -69,7 +69,7 @@
   }
 
   $: if (subIssues) {
-    const doneStatuses = $statusStore
+    const doneStatuses = $statusStore.statuses
       .filter((s) => s.category === tracker.issueStatusCategory.Completed)
       .map((p) => p._id)
     countComplete = subIssues.filter((si) => doneStatuses.includes(si.status)).length
@@ -101,7 +101,7 @@
           value: subIssues.map((iss) => {
             const text = currentProject ? `${getIssueId(currentProject, iss)} ${iss.title}` : iss.title
 
-            return { id: iss._id, text, isSelected: false, ...getIssueStatusIcon(iss, $statusByIdStore) }
+            return { id: iss._id, text, isSelected: false, ...getIssueStatusIcon(iss, $statusStore.byId) }
           }),
           width: 'large'
         },
diff --git a/plugins/tracker-resources/src/components/sprints/IssueStatistics.svelte b/plugins/tracker-resources/src/components/sprints/IssueStatistics.svelte
index 24fdd1b168..fb141de7fc 100644
--- a/plugins/tracker-resources/src/components/sprints/IssueStatistics.svelte
+++ b/plugins/tracker-resources/src/components/sprints/IssueStatistics.svelte
@@ -17,7 +17,7 @@
   import { Issue } from '@hcengineering/tracker'
   import { floorFractionDigits, Label } from '@hcengineering/ui'
   import tracker from '../../plugin'
-  import { statusByIdStore } from '../../utils'
+  import { statusStore } from '../../utils'
   import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
   import TimePresenter from '../issues/timereport/TimePresenter.svelte'
   export let docs: Issue[] | undefined = undefined
@@ -28,13 +28,13 @@
   $: noParents = docs?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
 
   $: rootNoBacklogIssues = noParents?.filter(
-    (it) => $statusByIdStore.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
+    (it) => $statusStore.byId.get(it.status)?.category !== tracker.issueStatusCategory.Backlog
   )
 
   $: totalEstimation = floorFractionDigits(
     (rootNoBacklogIssues ?? [{ estimation: 0, childInfo: [] } as unknown as Issue])
       .map((it) => {
-        const cat = $statusByIdStore.get(it.status)?.category
+        const cat = $statusStore.byId.get(it.status)?.category
 
         let retEst = it.estimation
         if (it.childInfo?.length > 0) {
diff --git a/plugins/tracker-resources/src/components/workflow/Statuses.svelte b/plugins/tracker-resources/src/components/workflow/Statuses.svelte
index 1562b79812..8a06529888 100644
--- a/plugins/tracker-resources/src/components/workflow/Statuses.svelte
+++ b/plugins/tracker-resources/src/components/workflow/Statuses.svelte
@@ -66,10 +66,10 @@
   }
 
   async function addStatus () {
-    if (editingStatus?.name && editingStatus?.category && $statusStore) {
-      const categoryStatuses = $statusStore.filter((s) => s.category === editingStatus!.category)
+    if (editingStatus?.name && editingStatus?.category) {
+      const categoryStatuses = $statusStore.statuses.filter((s) => s.category === editingStatus!.category)
       const prevStatus = categoryStatuses[categoryStatuses.length - 1]
-      const nextStatus = $statusStore[$statusStore.findIndex(({ _id }) => _id === prevStatus._id) + 1]
+      const nextStatus = $statusStore.statuses[$statusStore.statuses.findIndex(({ _id }) => _id === prevStatus._id) + 1]
 
       isSaving = true
       await client.addCollection(
@@ -93,9 +93,9 @@
   }
 
   async function editStatus () {
-    if ($statusStore && statusCategories && editingStatus?.name && editingStatus?.category && '_id' in editingStatus) {
+    if (statusCategories && editingStatus?.name && editingStatus?.category && '_id' in editingStatus) {
       const statusId = '_id' in editingStatus ? editingStatus._id : undefined
-      const status = statusId && $statusStore.find(({ _id }) => _id === statusId)
+      const status = statusId && $statusStore.byId.get(statusId)
 
       if (!status) {
         return
@@ -157,12 +157,14 @@
         },
         undefined,
         async (result) => {
-          if (result && project && $statusStore) {
+          if (result && project) {
             isSaving = true
             await client.removeDoc(status._class, status.space, status._id)
 
             if (project.defaultIssueStatus === status._id) {
-              const newDefaultStatus = $statusStore.find((s) => s._id !== status._id && s.category === status.category)
+              const newDefaultStatus = $statusStore.statuses.find(
+                (s) => s._id !== status._id && s.category === status.category && s.space === status.space
+              )
               if (newDefaultStatus?._id) {
                 await updateProjectDefaultStatus(newDefaultStatus._id)
               }
@@ -196,12 +198,12 @@
   }
 
   async function handleDrop (toItem: IssueStatus) {
-    if ($statusStore && draggingStatus?._id !== toItem._id && draggingStatus?.category === toItem.category) {
+    if (draggingStatus?._id !== toItem._id && draggingStatus?.category === toItem.category) {
       const fromIndex = getStatusIndex(draggingStatus)
       const toIndex = getStatusIndex(toItem)
       const [prev, next] = [
-        $statusStore[fromIndex < toIndex ? toIndex : toIndex - 1],
-        $statusStore[fromIndex < toIndex ? toIndex + 1 : toIndex]
+        $statusStore.statuses[fromIndex < toIndex ? toIndex : toIndex - 1],
+        $statusStore.statuses[fromIndex < toIndex ? toIndex + 1 : toIndex]
       ]
 
       isSaving = true
@@ -213,7 +215,7 @@
   }
 
   function getStatusIndex (status: IssueStatus) {
-    return $statusStore?.findIndex(({ _id }) => _id === status._id) ?? -1
+    return $statusStore.statuses.findIndex(({ _id }) => _id === status._id) ?? -1
   }
 
   function resetDrag () {
@@ -242,14 +244,14 @@
     </div>
   </svelte:fragment>
 
-  {#if project === undefined || statusCategories === undefined || $statusStore === undefined}
+  {#if project === undefined || statusCategories === undefined || $statusStore.statuses.length === 0}
     <Loading />
   {:else}
     <Scroller>
       <div class="popupPanel-body__main-content py-10 clear-mins">
         {#each statusCategories as category}
           {@const statuses =
-            $statusStore?.filter((s) => s.attachedTo === projectId && s.category === category._id) ?? []}
+            $statusStore.statuses.filter((s) => s.attachedTo === projectId && s.category === category._id) ?? []}
           {@const isSingle = statuses.length === 1}
           <div class="flex-between category-name">
             <Label label={category.label} />
diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts
index 425fdd1520..adebb9c964 100644
--- a/plugins/tracker-resources/src/utils.ts
+++ b/plugins/tracker-resources/src/utils.ts
@@ -677,16 +677,27 @@ export async function removeProject (project: Project): Promise<void> {
   await client.removeDoc(tracker.class.Project, core.space.Space, project._id)
 }
 
+/**
+ * @public
+ */
+export interface StatusStore {
+  statuses: Array<WithLookup<IssueStatus>>
+  byId: IdMap<WithLookup<IssueStatus>>
+  version: number
+}
 // Issue status live query
-export const statusByIdStore = writable<IdMap<WithLookup<IssueStatus>>>(new Map())
-export const statusStore = writable<Array<WithLookup<IssueStatus>>>([])
+export const statusStore = writable<StatusStore>({ statuses: [], byId: new Map(), version: 0 })
+
 const query = createQuery(true)
 query.query(
   tracker.class.IssueStatus,
   {},
   (res) => {
-    statusStore.set(res)
-    statusByIdStore.set(toIdMap(res))
+    statusStore.update((old) => ({
+      version: old.version + 1,
+      statuses: res,
+      byId: toIdMap(res)
+    }))
   },
   {
     lookup: {
diff --git a/plugins/tracker/src/utils.ts b/plugins/tracker/src/utils.ts
index 00efb0d0c1..e56292153c 100644
--- a/plugins/tracker/src/utils.ts
+++ b/plugins/tracker/src/utils.ts
@@ -39,6 +39,8 @@ export const genRanks = (count: number): Generator<string, void, unknown> =>
 export const calcRank = (prev?: { rank: string }, next?: { rank: string }): string => {
   const a = prev?.rank !== undefined ? LexoRank.parse(prev.rank) : LexoRank.min()
   const b = next?.rank !== undefined ? LexoRank.parse(next.rank) : LexoRank.max()
-
+  if (a.equals(b)) {
+    return a.genNext().toString()
+  }
   return a.between(b).toString()
 }
diff --git a/plugins/workbench-resources/src/connect.ts b/plugins/workbench-resources/src/connect.ts
index 205a34c1d2..4988bbe2a9 100644
--- a/plugins/workbench-resources/src/connect.ts
+++ b/plugins/workbench-resources/src/connect.ts
@@ -39,6 +39,8 @@ export async function connect (title: string): Promise<Client | undefined> {
   }
   _token = token
 
+  let clientSet = false
+
   const clientFactory = await getResource(client.function.GetClient)
   _client = await clientFactory(
     token,
@@ -54,7 +56,15 @@ export async function connect (title: string): Promise<Client | undefined> {
       })
     },
     // We need to refresh all active live queries and clear old queries.
-    refreshClient
+    () => {
+      try {
+        if (clientSet) {
+          refreshClient()
+        }
+      } catch (err) {
+        console.error(err)
+      }
+    }
   )
   console.log('logging in as', email)
 
@@ -72,6 +82,7 @@ export async function connect (title: string): Promise<Client | undefined> {
 
     // Update on connect, so it will be triggered
     setClient(_client)
+    clientSet = true
     return
   }
 
@@ -105,6 +116,7 @@ export async function connect (title: string): Promise<Client | undefined> {
 
   return _client
 }
+
 function clearMetadata (ws: string): void {
   const tokens = fetchMetadataLocalStorage(login.metadata.LoginTokens)
   if (tokens !== null) {
diff --git a/tests/sanity/package.json b/tests/sanity/package.json
index bb362d7ab9..a4b7256b6e 100644
--- a/tests/sanity/package.json
+++ b/tests/sanity/package.json
@@ -16,8 +16,8 @@
     "dev-uitest": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 SETTING=storage-dev.json playwright test --browser chromium --reporter list,html -c ./tests/playwright.config.ts",
     "debug": "playwright test --browser chromium -c ./tests/playwright.config.ts --debug --headed",
     "dev-debug": "cross-env PLATFORM_URI=http://localhost:8080 PLATFORM_TRANSACTOR=ws://localhost:3333 SETTING=storage-dev.json playwright test --browser chromium -c ./tests/playwright.config.ts --debug --headed",
-    "codegen": "playwright codegen --load-storage storage.json http://localhost:8083/workbench",
-    "dev-codegen": "cross-env playwright codegen --load-storage storage-dev.json http://localhost:8080/workbench"
+    "codegen": "playwright codegen --load-storage storage.json http://localhost:8083/workbench/sanity-ws/",
+    "dev-codegen": "cross-env playwright codegen --load-storage storage-dev.json http://localhost:8080/workbench/sanity-ws/"
   },
   "devDependencies": {
     "@hcengineering/platform-rig": "^0.6.0",
diff --git a/tests/sanity/tests/tracker.spec.ts b/tests/sanity/tests/tracker.spec.ts
index 4a0e900efc..aff9fd0627 100644
--- a/tests/sanity/tests/tracker.spec.ts
+++ b/tests/sanity/tests/tracker.spec.ts
@@ -205,9 +205,7 @@ test('create-issue-draft', async ({ page }) => {
 
   // Click text=Issues >> nth=1
   await page.locator('text=Issues').nth(1).click()
-  await expect(page).toHaveURL(
-    'http://localhost:8083/workbench/sanity-ws/tracker/tracker%3Aproject%3ADefaultProject/issues'
-  )
+  await expect(page).toHaveURL(/.*\/workbench\/sanity-ws\/tracker\/tracker%3Aproject%3ADefaultProject\/issues/)
   await expect(page.locator('#new-issue')).toHaveText('New issue')
   // Click button:has-text("New issue")
   await page.locator('#new-issue').click()