From b8fbcc695e0e868120528f346695115fe440dc6f Mon Sep 17 00:00:00 2001
From: Denis Bykhov <bykhov.denis@gmail.com>
Date: Fri, 6 Oct 2023 16:29:25 +0600
Subject: [PATCH] UBER-1000 (#3801)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
---
 plugins/contact-assets/lang/en.json           |  2 -
 plugins/contact-assets/lang/ru.json           |  2 -
 plugins/contact-resources/src/assignee.ts     | 35 ++------
 .../src/components/AssigneeBox.svelte         | 11 +--
 .../src/components/AssigneePopup.svelte       | 86 +++++++++---------
 plugins/contact-resources/src/plugin.ts       |  2 -
 plugins/notification-resources/src/utils.ts   |  3 +-
 plugins/tracker-assets/lang/en.json           |  1 +
 plugins/tracker-assets/lang/ru.json           |  1 +
 .../components/issues/AssigneeEditor.svelte   | 87 ++++++++++---------
 plugins/tracker-resources/src/plugin.ts       |  3 +-
 plugins/tracker-resources/src/utils.ts        | 64 ++++++++------
 plugins/view-resources/src/actions.ts         | 38 ++++----
 .../src/components/Workbench.svelte           | 17 +---
 14 files changed, 169 insertions(+), 183 deletions(-)

diff --git a/plugins/contact-assets/lang/en.json b/plugins/contact-assets/lang/en.json
index 8185683a40..1eb262a5f6 100644
--- a/plugins/contact-assets/lang/en.json
+++ b/plugins/contact-assets/lang/en.json
@@ -90,8 +90,6 @@
     "AddMembersHeader": "Add members to {value}:",
     "Assigned": "Assigned",
     "Unassigned": "Unassigned",
-    "CategoryPreviousAssigned": "Previously assigned",
-    "CategoryComponentLead": "Component lead",
     "CategoryCurrentUser": "Current user",    
     "CategoryOther": "Other",
     "NumberMembers": "{count, plural, =0 {no members} =1 {1 member} other {# members}}",
diff --git a/plugins/contact-assets/lang/ru.json b/plugins/contact-assets/lang/ru.json
index 2b1332923c..630ac97851 100644
--- a/plugins/contact-assets/lang/ru.json
+++ b/plugins/contact-assets/lang/ru.json
@@ -92,8 +92,6 @@
     "Assigned": "Назначен",
     "Unassigned": "Не назначен",
     "CategoryCurrentUser": "Текущий пользователь",
-    "CategoryPreviousAssigned": "Ранее назначенные",
-    "CategoryComponentLead": "Ответственный за компонент",
     "CategoryOther": "Прочие",
     "Position": "Должность",
     "ConfigLabel": "Контакты",
diff --git a/plugins/contact-resources/src/assignee.ts b/plugins/contact-resources/src/assignee.ts
index 7eefb560db..cd245c96da 100644
--- a/plugins/contact-resources/src/assignee.ts
+++ b/plugins/contact-resources/src/assignee.ts
@@ -1,36 +1,11 @@
+import { Person } from '@hcengineering/contact'
+import { Ref } from '@hcengineering/core'
 import { IntlString } from '@hcengineering/platform'
-import contact from './plugin'
 
 /**
  * @public
  */
-export type AssigneeCategory = 'CurrentUser' | 'Assigned' | 'PreviouslyAssigned' | 'ComponentLead' | 'Members' | 'Other'
-
-const assigneeCategoryTitleMap: Record<AssigneeCategory, IntlString> = Object.freeze({
-  CurrentUser: contact.string.CategoryCurrentUser,
-  Assigned: contact.string.Assigned,
-  PreviouslyAssigned: contact.string.CategoryPreviousAssigned,
-  ComponentLead: contact.string.CategoryComponentLead,
-  Members: contact.string.Members,
-  Other: contact.string.CategoryOther
-})
-
-/**
- * @public
- */
-export const assigneeCategoryOrder: AssigneeCategory[] = [
-  'CurrentUser',
-  'Assigned',
-  'PreviouslyAssigned',
-  'ComponentLead',
-  'Members',
-  'Other'
-]
-
-/**
- * @public
- */
-export function getCategoryTitle (category: AssigneeCategory | undefined): IntlString {
-  const cat: AssigneeCategory = category ?? 'Other'
-  return assigneeCategoryTitleMap[cat]
+export interface AssigneeCategory {
+  label: IntlString
+  func: (val: Array<Ref<Person>>) => Promise<Array<Ref<Person>>>
 }
diff --git a/plugins/contact-resources/src/components/AssigneeBox.svelte b/plugins/contact-resources/src/components/AssigneeBox.svelte
index d77b08374b..172c40db3a 100644
--- a/plugins/contact-resources/src/components/AssigneeBox.svelte
+++ b/plugins/contact-resources/src/components/AssigneeBox.svelte
@@ -39,6 +39,7 @@
   import EmployeePresenter from './EmployeePresenter.svelte'
   import UserInfo from './UserInfo.svelte'
   import IconPerson from './icons/Person.svelte'
+  import { AssigneeCategory } from '../assignee'
 
   export let _class: Ref<Class<Employee>> = contact.mixin.Employee
   export let excluded: Ref<Contact>[] | undefined = undefined
@@ -49,9 +50,7 @@
   export let label: IntlString
   export let placeholder: IntlString = presentation.string.Search
   export let value: Ref<Person> | null | undefined
-  export let prevAssigned: Ref<Person>[] | undefined = []
-  export let componentLead: Ref<Employee> | undefined = undefined
-  export let members: Ref<Employee>[] | undefined = []
+  export let categories: AssigneeCategory[] | undefined = undefined
   export let allowDeselect = true
   export let titleDeselect: IntlString | undefined = undefined
   export let readonly = false
@@ -87,7 +86,7 @@
 
   const mgr = getFocusManager()
 
-  const _click = (ev: MouseEvent): void => {
+  function _click (ev: MouseEvent): void {
     if (!readonly) {
       ev.preventDefault()
       ev.stopPropagation()
@@ -98,9 +97,7 @@
           _class,
           options,
           docQuery,
-          prevAssigned,
-          componentLead,
-          members,
+          categories,
           ignoreUsers: excluded ?? [],
           icon,
           selected: value,
diff --git a/plugins/contact-resources/src/components/AssigneePopup.svelte b/plugins/contact-resources/src/components/AssigneePopup.svelte
index 549eb7a933..d22679ffa0 100644
--- a/plugins/contact-resources/src/components/AssigneePopup.svelte
+++ b/plugins/contact-resources/src/components/AssigneePopup.svelte
@@ -13,34 +13,33 @@
 // limitations under the License.
 -->
 <script lang="ts">
-  import contact, { Contact, PersonAccount, Person, Employee } from '@hcengineering/contact'
-  import { DocumentQuery, FindOptions, getCurrentAccount, Ref } from '@hcengineering/core'
+  import { Contact, Person, PersonAccount } from '@hcengineering/contact'
+  import { DocumentQuery, FindOptions, Ref, getCurrentAccount } from '@hcengineering/core'
   import type { Asset, IntlString } from '@hcengineering/platform'
+  import presentation, { createQuery } from '@hcengineering/presentation'
   import {
-    createFocusManager,
+    AnySvelteComponent,
     EditWithIcon,
     FocusHandler,
     Icon,
     IconCheck,
     IconSearch,
-    deviceOptionsStore,
-    ListView,
-    resizeObserver,
-    AnySvelteComponent,
     Label,
+    ListView,
+    createFocusManager,
+    deviceOptionsStore,
+    resizeObserver,
     tooltip
   } from '@hcengineering/ui'
-  import presentation, { createQuery } from '@hcengineering/presentation'
   import { createEventDispatcher } from 'svelte'
-  import { AssigneeCategory, assigneeCategoryOrder, getCategoryTitle } from '../assignee'
+  import { AssigneeCategory } from '../assignee'
+  import contact from '../plugin'
   import UserInfo from './UserInfo.svelte'
 
   export let options: FindOptions<Contact> | undefined = undefined
   export let selected: Ref<Person> | undefined
   export let docQuery: DocumentQuery<Contact> | undefined = undefined
-  export let prevAssigned: Ref<Employee>[] | undefined = []
-  export let componentLead: Ref<Employee> | undefined = undefined
-  export let members: Ref<Employee>[] | undefined = []
+  export let categories: AssigneeCategory[] | undefined = undefined
   export let allowDeselect = true
   export let titleDeselect: IntlString | undefined
   export let placeholder: IntlString = presentation.string.Search
@@ -49,16 +48,15 @@
   export let shadows: boolean = true
   export let width: 'medium' | 'large' | 'full' = 'medium'
   export let searchField: string = 'name'
-  export let showCategories: boolean = true
   export let icon: Asset | AnySvelteComponent | undefined = undefined
 
-  const currentEmployee = (getCurrentAccount() as PersonAccount).person
+  $: showCategories = categories !== undefined && categories.length > 0
 
   let search: string = ''
   let objects: Contact[] = []
   let contacts: Contact[] = []
 
-  let categorizedPersons: Map<Ref<Person>, AssigneeCategory>
+  const categorizedPersons: Map<Ref<Person>, AssigneeCategory> = new Map()
 
   const dispatch = createEventDispatcher()
   const query = createQuery()
@@ -79,29 +77,40 @@
     { ...(options ?? {}), limit: 200, sort: { name: 1 } }
   )
 
-  $: updateCategories(objects, currentEmployee, prevAssigned, componentLead, members)
+  $: updateCategories(objects, categories)
 
-  function updateCategories (
-    objects: Contact[],
-    currentEmployee: Ref<Person>,
-    prevAssigned: Ref<Person>[] | undefined,
-    componentLead: Ref<Person> | undefined,
-    members: Ref<Person>[] | undefined
-  ) {
-    const persons = new Map<Ref<Person>, AssigneeCategory>(objects.map((t) => [t._id, 'Other']))
-    if (componentLead) {
-      persons.set(componentLead, 'ComponentLead')
+  const currentUserCategory: AssigneeCategory = {
+    label: contact.string.CategoryCurrentUser,
+    func: async () => {
+      const account = getCurrentAccount() as PersonAccount
+      return [account.person]
     }
-    members?.forEach((p) => persons.set(p, 'Members'))
-    prevAssigned?.forEach((p) => persons.set(p, 'PreviouslyAssigned'))
-    if (selected) {
-      persons.set(selected, 'Assigned')
-    }
-    persons.set(currentEmployee, 'CurrentUser')
+  }
 
-    categorizedPersons = new Map<Ref<Person>, AssigneeCategory>(
-      [...persons].sort((a, b) => assigneeCategoryOrder.indexOf(a[1]) - assigneeCategoryOrder.indexOf(b[1]))
-    )
+  const assigned: AssigneeCategory = {
+    label: contact.string.Assigned,
+    func: async () => {
+      return selected ? [selected] : []
+    }
+  }
+
+  const otherCategory: AssigneeCategory = {
+    label: contact.string.CategoryOther,
+    func: async (val: Ref<Contact>[]) => {
+      return val
+    }
+  }
+
+  async function updateCategories (objects: Contact[], categories: AssigneeCategory[] | undefined) {
+    const refs = objects.map((e) => e._id)
+
+    for (const category of [currentUserCategory, assigned, ...(categories ?? []), otherCategory]) {
+      const res = await category.func(refs)
+      for (const contact of res) {
+        if (categorizedPersons.has(contact)) continue
+        categorizedPersons.set(contact, category)
+      }
+    }
     contacts = []
     categorizedPersons.forEach((p, k) => {
       const c = objects.find((e) => e._id === k)
@@ -176,15 +185,12 @@
             {@const obj = toAny(contacts[item])}
             {@const category = categorizedPersons.get(obj._id)}
             <!-- {@const cl = hierarchy.getClass(contacts[item]._class)} -->
-            {#if item === 0 || (item > 0 && categorizedPersons.get(toAny(contacts[item - 1])._id) !== categorizedPersons.get(obj._id))}
+            {#if category !== undefined && (item === 0 || (item > 0 && categorizedPersons.get(toAny(contacts[item - 1])._id) !== categorizedPersons.get(obj._id)))}
               <!--Category for first item-->
               {#if item > 0}<div class="menu-separator" />{/if}
               <div class="menu-group__header flex-row-center category-box">
-                <!-- {#if cl.icon}
-                  <div class="clear-mins mr-2"><Icon icon={cl.icon} size={'small'} /></div>
-                {/if} -->
                 <span class="overflow-label">
-                  <Label label={getCategoryTitle(category)} />
+                  <Label label={category.label} />
                 </span>
               </div>
             {/if}
diff --git a/plugins/contact-resources/src/plugin.ts b/plugins/contact-resources/src/plugin.ts
index 5c11f93c55..95ad496eaf 100644
--- a/plugins/contact-resources/src/plugin.ts
+++ b/plugins/contact-resources/src/plugin.ts
@@ -74,8 +74,6 @@ export default mergeIds(contactId, contact, {
     Assigned: '' as IntlString,
     Unassigned: '' as IntlString,
     CategoryCurrentUser: '' as IntlString,
-    CategoryPreviousAssigned: '' as IntlString,
-    CategoryComponentLead: '' as IntlString,
     CategoryOther: '' as IntlString,
     DeleteEmployee: '' as IntlString,
     DeleteEmployeeDescr: '' as IntlString,
diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts
index 77b35d96a9..ca14a909c6 100644
--- a/plugins/notification-resources/src/utils.ts
+++ b/plugins/notification-resources/src/utils.ts
@@ -47,8 +47,9 @@ export class NotificationClientImpl implements NotificationClient {
     )
   }
 
-  static createClient (): void {
+  static createClient (): NotificationClientImpl {
     NotificationClientImpl._instance = new NotificationClientImpl()
+    return NotificationClientImpl._instance
   }
 
   static getClient (): NotificationClientImpl {
diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json
index 2ebba7db4d..24c8d98239 100644
--- a/plugins/tracker-assets/lang/en.json
+++ b/plugins/tracker-assets/lang/en.json
@@ -282,6 +282,7 @@
     "IssueNotificationChanged": "{senderName} changed {property}",
     "IssueNotificationChangedProperty": "{senderName} changed {property} to \"{newValue}\"",
     "IssueNotificationMessage": "{senderName}: {message}",
+    "PreviousAssigned": "Previously assigned",
     "IssueAssigneedToYou": "Assigned to you"
   },
   "status": {}
diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json
index 92fd268755..34b29185e6 100644
--- a/plugins/tracker-assets/lang/ru.json
+++ b/plugins/tracker-assets/lang/ru.json
@@ -282,6 +282,7 @@
     "IssueNotificationChanged": "{senderName} изменил {property}",
     "IssueNotificationChangedProperty": "{senderName} изменил {property} на \"{newValue}\"",
     "IssueNotificationMessage": "{senderName}: {message}",
+    "PreviousAssigned": "Ранее назначенные",
     "IssueAssigneedToYou": "Назначено вам"
   },
   "status": {}
diff --git a/plugins/tracker-resources/src/components/issues/AssigneeEditor.svelte b/plugins/tracker-resources/src/components/issues/AssigneeEditor.svelte
index 5282145fe0..7a609915b6 100644
--- a/plugins/tracker-resources/src/components/issues/AssigneeEditor.svelte
+++ b/plugins/tracker-resources/src/components/issues/AssigneeEditor.svelte
@@ -13,15 +13,17 @@
 // limitations under the License.
 -->
 <script lang="ts">
-  import { Employee, PersonAccount, Person } from '@hcengineering/contact'
+  import { Employee, Person, PersonAccount } from '@hcengineering/contact'
   import { AssigneeBox, personAccountByIdStore } from '@hcengineering/contact-resources'
+  import { AssigneeCategory } from '@hcengineering/contact-resources/src/assignee'
   import { Doc, DocumentQuery, Ref } from '@hcengineering/core'
-  import { createQuery, getClient } from '@hcengineering/presentation'
-  import { Issue, Project } from '@hcengineering/tracker'
+  import { getClient } from '@hcengineering/presentation'
+  import { Issue } from '@hcengineering/tracker'
   import { ButtonKind, ButtonSize, IconSize, TooltipAlignment } from '@hcengineering/ui'
   import { createEventDispatcher } from 'svelte'
   import tracker from '../../plugin'
   import { getPreviousAssignees } from '../../utils'
+  import { get } from 'svelte/store'
 
   type Object = (Doc | {}) & Pick<Issue, 'space' | 'component' | 'assignee'>
 
@@ -38,37 +40,8 @@
 
   const client = getClient()
   const dispatch = createEventDispatcher()
-  const projectQuery = createQuery()
 
-  let project: Project | undefined
-  let prevAssigned: Ref<Person>[] = []
-  let componentLead: Ref<Employee> | undefined = undefined
-  let members: Ref<Employee>[] = []
-  let docQuery: DocumentQuery<Employee> = {}
-
-  $: '_class' in object &&
-    getPreviousAssignees(object._id).then((res) => {
-      prevAssigned = res
-    })
-
-  async function updateComponentMembers (project: Project, issue: Object) {
-    if (issue.component) {
-      const component = await client.findOne(tracker.class.Component, { _id: issue.component })
-      componentLead = component?.lead || undefined
-    } else {
-      componentLead = undefined
-    }
-    if (project !== undefined) {
-      const accounts = project.members
-        .map((p) => $personAccountByIdStore.get(p as Ref<PersonAccount>))
-        .filter((p) => p !== undefined) as PersonAccount[]
-      members = accounts.map((p) => p.person as Ref<Employee>)
-    } else {
-      members = []
-    }
-
-    docQuery = project?.private ? { _id: { $in: members } } : { active: true }
-  }
+  const docQuery: DocumentQuery<Employee> = { active: true }
 
   const handleAssigneeChanged = async (newAssignee: Ref<Person> | undefined) => {
     if (newAssignee === undefined || object.assignee === newAssignee) {
@@ -82,9 +55,47 @@
     }
   }
 
-  $: projectQuery.query(tracker.class.Project, { _id: object.space }, (res) => ([project] = res))
-  $: project && updateComponentMembers(project, object)
-  $: docQuery = project?.private ? { _id: { $in: members } } : {}
+  let categories: AssigneeCategory[] = []
+
+  function getCategories (object: Object): void {
+    categories = []
+    if ('_class' in object) {
+      const _id = object._id
+      categories.push({
+        label: tracker.string.PreviousAssigned,
+        func: async () => await getPreviousAssignees(_id)
+      })
+    }
+    categories.push({
+      label: tracker.string.ComponentLead,
+      func: async () => {
+        if (!object.component) {
+          return []
+        }
+        const component = await client.findOne(tracker.class.Component, { _id: object.component })
+        return component?.lead ? [component.lead] : []
+      }
+    })
+    categories.push({
+      label: tracker.string.Members,
+      func: async () => {
+        if (!object.space) {
+          return []
+        }
+        const project = await client.findOne(tracker.class.Project, { _id: object.space })
+        if (project === undefined) {
+          return []
+        }
+        const store = get(personAccountByIdStore)
+        const accounts = project.members
+          .map((p) => store.get(p as Ref<PersonAccount>))
+          .filter((p) => p !== undefined) as PersonAccount[]
+        return accounts.map((p) => p.person as Ref<Employee>)
+      }
+    })
+  }
+
+  $: getCategories(object)
 </script>
 
 {#if object}
@@ -94,9 +105,7 @@
     label={tracker.string.Assignee}
     placeholder={tracker.string.Assignee}
     value={object.assignee}
-    {prevAssigned}
-    {componentLead}
-    {members}
+    {categories}
     titleDeselect={tracker.string.Unassigned}
     {size}
     {kind}
diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts
index fcbea42014..d4a6a51bbb 100644
--- a/plugins/tracker-resources/src/plugin.ts
+++ b/plugins/tracker-resources/src/plugin.ts
@@ -300,7 +300,8 @@ export default mergeIds(trackerId, tracker, {
 
     NoStatusFound: '' as IntlString,
     CreateMissingStatus: '' as IntlString,
-    UnsetParent: '' as IntlString
+    UnsetParent: '' as IntlString,
+    PreviousAssigned: '' as IntlString
   },
   component: {
     NopeComponent: '' as AnyComponent,
diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts
index 53528e8c9f..8b7ef0c1cb 100644
--- a/plugins/tracker-resources/src/utils.ts
+++ b/plugins/tracker-resources/src/utils.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 //
 
-import { Employee } from '@hcengineering/contact'
+import { Contact } from '@hcengineering/contact'
 import core, {
   ApplyOperations,
   AttachedData,
@@ -29,23 +29,24 @@ import core, {
   Space,
   Status,
   StatusCategory,
-  toIdMap,
   TxCollectionCUD,
+  TxCreateDoc,
   TxOperations,
   TxResult,
-  TxUpdateDoc
+  TxUpdateDoc,
+  toIdMap
 } from '@hcengineering/core'
 import { Asset, IntlString } from '@hcengineering/platform'
-import { createQuery } from '@hcengineering/presentation'
+import { createQuery, getClient } from '@hcengineering/presentation'
 import { calcRank } from '@hcengineering/task'
 import {
   Component,
   Issue,
   IssuePriority,
+  IssueStatus,
   IssuesDateModificationPeriod,
   IssuesGrouping,
   IssuesOrdering,
-  IssueStatus,
   Milestone,
   MilestoneStatus,
   Project,
@@ -54,18 +55,18 @@ import {
 import {
   AnyComponent,
   AnySvelteComponent,
+  MILLISECONDS_IN_WEEK,
+  PaletteColorIndexes,
   areDatesEqual,
   getMillisecondsInMonth,
-  isWeekend,
-  MILLISECONDS_IN_WEEK,
-  PaletteColorIndexes
+  isWeekend
 } from '@hcengineering/ui'
 import { KeyFilter, ViewletDescriptor } from '@hcengineering/view'
 import {
   CategoryQuery,
-  groupBy,
   ListSelectionProvider,
   SelectDirection,
+  groupBy,
   statusStore
 } from '@hcengineering/view-resources'
 import { get } from 'svelte/store'
@@ -497,24 +498,33 @@ export function subIssueListProvider (subIssues: Issue[], target: Ref<Issue>): v
   }
 }
 
-export async function getPreviousAssignees (objectId: Ref<Doc>): Promise<Array<Ref<Employee>>> {
-  return await new Promise((resolve) => {
-    const query = createQuery(true)
-    query.query(
-      core.class.Tx,
-      {
-        'tx.objectId': objectId,
-        'tx.operations.assignee': { $exists: true }
-      },
-      (res) => {
-        const prevAssignee = res
-          .map((t) => ((t as TxCollectionCUD<Doc, Issue>).tx as TxUpdateDoc<Issue>).operations.assignee)
-          .filter((p) => !(p == null)) as Array<Ref<Employee>>
-        resolve(prevAssignee)
-        query.unsubscribe()
-      }
-    )
-  })
+export async function getPreviousAssignees (objectId: Ref<Doc> | undefined): Promise<Array<Ref<Contact>>> {
+  if (objectId === undefined) {
+    return []
+  }
+  const client = getClient()
+  const createTx = (
+    await client.findAll<TxCollectionCUD<Issue, Issue>>(core.class.TxCollectionCUD, {
+      'tx.objectId': objectId,
+      'tx._class': core.class.TxCreateDoc
+    })
+  )[0]
+  const updateTxes = await client.findAll<TxCollectionCUD<Issue, Issue>>(
+    core.class.TxCollectionCUD,
+    { 'tx.objectId': objectId, 'tx._class': core.class.TxUpdateDoc, 'tx.operations.assignee': { $exists: true } },
+    { sort: { modifiedOn: -1 } }
+  )
+  const set: Set<Ref<Contact>> = new Set()
+  const createAssignee = (createTx.tx as TxCreateDoc<Issue>).attributes.assignee
+  for (const tx of updateTxes) {
+    const assignee = (tx.tx as TxUpdateDoc<Issue>).operations.assignee
+    if (assignee == null) continue
+    set.add(assignee)
+  }
+  if (createAssignee != null) {
+    set.add(createAssignee)
+  }
+  return Array.from(set)
 }
 
 async function updateIssuesOnMove (
diff --git a/plugins/view-resources/src/actions.ts b/plugins/view-resources/src/actions.ts
index 17225e651e..d950cd6ba1 100644
--- a/plugins/view-resources/src/actions.ts
+++ b/plugins/view-resources/src/actions.ts
@@ -56,14 +56,31 @@ export async function getActions (
   derived: Ref<Class<Doc>> = core.class.Doc,
   mode: ViewContextType = 'context'
 ): Promise<Action[]> {
-  const actions: Action[] = await client.findAll(view.class.Action, {
+  let actions: Action[] = await client.findAll(view.class.Action, {
     'context.mode': mode
   })
 
   const categories: Partial<Record<ActionGroup | 'top', number>> = { top: 1, tools: 50, other: 100, remove: 200 }
 
-  let filteredActions: Action[] = []
+  if (Array.isArray(doc)) {
+    for (const d of doc) {
+      actions = filterActions(client, d, actions, derived)
+    }
+  } else {
+    actions = filterActions(client, doc, actions, derived)
+  }
+  const inputVal: ViewActionInput[] = ['none']
+  if (!Array.isArray(doc) || doc.length === 1) {
+    inputVal.push('focus')
+    inputVal.push('any')
+  }
+  if (Array.isArray(doc) && doc.length > 0) {
+    inputVal.push('selection')
+    inputVal.push('any')
+  }
+  actions = actions.filter((it) => inputVal.includes(it.input))
 
+  const filteredActions: Action[] = []
   for (const action of actions) {
     if (action.visibilityTester == null) {
       filteredActions.push(action)
@@ -75,23 +92,6 @@ export async function getActions (
       }
     }
   }
-  if (Array.isArray(doc)) {
-    for (const d of doc) {
-      filteredActions = filterActions(client, d, filteredActions, derived)
-    }
-  } else {
-    filteredActions = filterActions(client, doc, filteredActions, derived)
-  }
-  const inputVal: ViewActionInput[] = ['none']
-  if (!Array.isArray(doc) || doc.length === 1) {
-    inputVal.push('focus')
-    inputVal.push('any')
-  }
-  if (Array.isArray(doc) && doc.length > 0) {
-    inputVal.push('selection')
-    inputVal.push('any')
-  }
-  filteredActions = filteredActions.filter((it) => inputVal.includes(it.input))
   filteredActions.sort((a, b) => {
     const aTarget = categories[a.context.group ?? 'top'] ?? 0
     const bTarget = categories[b.context.group ?? 'top'] ?? 0
diff --git a/plugins/workbench-resources/src/components/Workbench.svelte b/plugins/workbench-resources/src/components/Workbench.svelte
index d717500141..043d81ed44 100644
--- a/plugins/workbench-resources/src/components/Workbench.svelte
+++ b/plugins/workbench-resources/src/components/Workbench.svelte
@@ -102,7 +102,6 @@
   const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
 
   const client = getClient()
-  NotificationClientImpl.createClient()
 
   let apps: Application[] | Promise<Application[]> = client
     .findAll(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
@@ -162,18 +161,10 @@
   )
 
   let hasNotification = false
-  const notificationQuery = createQuery()
-
-  notificationQuery.query(
-    notification.class.DocUpdates,
-    {
-      user: accountId,
-      hidden: false
-    },
-    (res) => {
-      hasNotification = res.some((p) => p.txes.some((p) => p.isNew))
-    }
-  )
+  const noficicationClient = NotificationClientImpl.getClient()
+  noficicationClient.docUpdates.subscribe((res) => {
+    hasNotification = res.some((p) => !p.hidden && p.txes.some((p) => p.isNew))
+  })
 
   const workspaceId = $location.path[1]