taskbarTabs.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import { tryOnScopeDispose } from '@vueuse/shared'
  3. import { isEqual, keyBy } from 'lodash-es'
  4. import { acceptHMRUpdate, defineStore } from 'pinia'
  5. import { computed, ref, watch } from 'vue'
  6. import { useRouter } from 'vue-router'
  7. import {
  8. EnumTaskbarApp,
  9. EnumTaskbarEntity,
  10. type UserCurrentTaskbarItemListQuery,
  11. type UserCurrentTaskbarItemListUpdatesSubscription,
  12. type UserCurrentTaskbarItemListUpdatesSubscriptionVariables,
  13. type UserCurrentTaskbarItemUpdatesSubscription,
  14. type UserCurrentTaskbarItemUpdatesSubscriptionVariables,
  15. } from '#shared/graphql/types.ts'
  16. import { convertToGraphQLId } from '#shared/graphql/utils.ts'
  17. import { getApolloClient } from '#shared/server/apollo/client.ts'
  18. import {
  19. MutationHandler,
  20. QueryHandler,
  21. } from '#shared/server/apollo/handler/index.ts'
  22. import { useApplicationStore } from '#shared/stores/application.ts'
  23. import type { ObjectWithId } from '#shared/types/utils.ts'
  24. import log from '#shared/utils/log.ts'
  25. import { userTaskbarTabPluginByType } from '#desktop/components/UserTaskbarTabs/plugins/index.ts'
  26. import type {
  27. BackRoute,
  28. UserTaskbarTab,
  29. } from '#desktop/components/UserTaskbarTabs/types.ts'
  30. import { useUserCurrentTaskbarItemAddMutation } from '../graphql/mutations/userCurrentTaskbarItemAdd.api.ts'
  31. import { useUserCurrentTaskbarItemDeleteMutation } from '../graphql/mutations/userCurrentTaskbarItemDelete.api.ts'
  32. import { useUserCurrentTaskbarItemTouchLastContactMutation } from '../graphql/mutations/userCurrentTaskbarItemTouchLastContact.api.ts'
  33. import { useUserCurrentTaskbarItemUpdateMutation } from '../graphql/mutations/userCurrentTaskbarItemUpdate.api.ts'
  34. import {
  35. UserCurrentTaskbarItemListDocument,
  36. useUserCurrentTaskbarItemListQuery,
  37. } from '../graphql/queries/userCurrentTaskbarItemList.api.ts'
  38. import { UserCurrentTaskbarItemListUpdatesDocument } from '../graphql/subscriptions/userCurrentTaskbarItemListUpdates.api.ts'
  39. import { UserCurrentTaskbarItemUpdatesDocument } from '../graphql/subscriptions/userCurrentTaskbarItemUpdates.api.ts'
  40. import type { TaskbarTabContext } from '../types.ts'
  41. export const useUserCurrentTaskbarTabsStore = defineStore(
  42. 'userCurrentTaskbarTabs',
  43. () => {
  44. const application = useApplicationStore()
  45. const router = useRouter()
  46. const taskbarTabContexts = ref<Record<string, TaskbarTabContext>>({})
  47. const activeTaskbarTabEntityKey = ref<string>()
  48. const taskbarTabsInCreation = ref<UserTaskbarTab[]>([])
  49. const taskbarTabIDsInDeletion = ref<ID[]>([])
  50. const getTaskbarTabTypePlugin = (tabEntityType: EnumTaskbarEntity) =>
  51. userTaskbarTabPluginByType[tabEntityType]
  52. const taskbarTabsQuery = new QueryHandler(
  53. useUserCurrentTaskbarItemListQuery(
  54. { app: EnumTaskbarApp.Desktop },
  55. {
  56. context: {
  57. batch: {
  58. active: false,
  59. },
  60. },
  61. },
  62. ),
  63. )
  64. const taskbarTabsRaw = taskbarTabsQuery.result()
  65. const taskbarTabsLoading = taskbarTabsQuery.loading()
  66. const taskbarTabList = computed<UserTaskbarTab[]>(
  67. (currentTaskbarTabList) => {
  68. if (!taskbarTabsRaw.value?.userCurrentTaskbarItemList) return []
  69. const taskbarTabs: UserTaskbarTab[] =
  70. taskbarTabsRaw.value.userCurrentTaskbarItemList
  71. .filter(
  72. (taskbarTab) =>
  73. !taskbarTabIDsInDeletion.value.includes(taskbarTab.id),
  74. )
  75. .flatMap((taskbarTab) => {
  76. const type = taskbarTab.callback
  77. if (!userTaskbarTabPluginByType[type]) {
  78. log.warn(`Unknown taskbar tab type: ${type}.`)
  79. return []
  80. }
  81. return {
  82. type,
  83. entity: taskbarTab.entity,
  84. entityAccess: taskbarTab.entityAccess,
  85. tabEntityKey: taskbarTab.key,
  86. taskbarTabId: taskbarTab.id,
  87. order: taskbarTab.prio,
  88. formId: taskbarTab.formId,
  89. formNewArticlePresent: taskbarTab.formNewArticlePresent,
  90. changed: taskbarTab.changed,
  91. dirty: taskbarTab.dirty,
  92. notify: taskbarTab.notify,
  93. updatedAt: taskbarTab.updatedAt,
  94. }
  95. })
  96. const existingTabEntityKeys = new Set(
  97. taskbarTabs.map((taskbarTab) => taskbarTab.tabEntityKey),
  98. )
  99. const newTaskbarTabList = taskbarTabs
  100. .concat(
  101. taskbarTabsInCreation.value.filter(
  102. (taskbarTab) =>
  103. !existingTabEntityKeys.has(taskbarTab.tabEntityKey),
  104. ),
  105. )
  106. .sort((a, b) => a.order - b.order)
  107. if (
  108. currentTaskbarTabList &&
  109. isEqual(currentTaskbarTabList, newTaskbarTabList)
  110. )
  111. return currentTaskbarTabList
  112. return newTaskbarTabList
  113. },
  114. )
  115. const activeTaskbarTab = computed<UserTaskbarTab | undefined>(
  116. (currentActiveTaskbarTab) => {
  117. if (!activeTaskbarTabEntityKey.value) return
  118. const newActiveTaskbarTab = taskbarTabList.value.find(
  119. (taskbarTab) =>
  120. taskbarTab.tabEntityKey === activeTaskbarTabEntityKey.value,
  121. )
  122. if (
  123. currentActiveTaskbarTab &&
  124. isEqual(newActiveTaskbarTab, currentActiveTaskbarTab)
  125. )
  126. return currentActiveTaskbarTab
  127. return newActiveTaskbarTab
  128. },
  129. )
  130. const activeTaskbarTabId = computed(
  131. () => activeTaskbarTab.value?.taskbarTabId,
  132. )
  133. const activeTaskbarTabEntityAccess = computed(
  134. () => activeTaskbarTab.value?.entityAccess,
  135. )
  136. const hasTaskbarTabs = computed(() => taskbarTabList.value?.length > 0)
  137. const taskbarTabListByTabEntityKey = computed(() =>
  138. keyBy(taskbarTabList.value, 'tabEntityKey'),
  139. )
  140. const taskbarTabListOrder = computed(() =>
  141. taskbarTabList.value.reduce((taskbarTabListOrder, taskbarTab) => {
  142. taskbarTabListOrder.push(taskbarTab.tabEntityKey)
  143. return taskbarTabListOrder
  144. }, [] as string[]),
  145. )
  146. const taskbarLookupByTypeAndTabEntityKey = computed(() => {
  147. return taskbarTabList.value.reduce(
  148. (lookup, tab) => {
  149. if (!tab.taskbarTabId) return lookup
  150. lookup[tab.type] = lookup[tab.type] || {}
  151. lookup[tab.type][tab.tabEntityKey] = tab.taskbarTabId
  152. return lookup
  153. },
  154. {} as Record<EnumTaskbarEntity, Record<ID, ID>>,
  155. )
  156. })
  157. const taskbarTabExists = (type: EnumTaskbarEntity, tabEntityKey: string) =>
  158. Boolean(taskbarLookupByTypeAndTabEntityKey.value[type]?.[tabEntityKey])
  159. const previousRoutes = ref<BackRoute[]>([])
  160. // Keep track of previously visited routes and if they are taskbar tab routes.
  161. router.afterEach((_, from) => {
  162. // Clear all previous routes whenever a non-taskbar tab route is visited.
  163. if (!from.meta?.taskbarTabEntityKey) previousRoutes.value.length = 0
  164. previousRoutes.value.push({
  165. path: from.fullPath,
  166. taskbarTabEntityKey: from.meta?.taskbarTabEntityKey,
  167. })
  168. })
  169. const backRoutes = computed(() => [...previousRoutes.value].reverse())
  170. const redirectToLastHistoricalRoute = () => {
  171. // In case of taskbar tab routes, make sure the tab is still present in the list.
  172. // We can do this by comparing the historical taskbar tab entity key against the current tab list.
  173. const nextRoute = backRoutes.value.find((backRoute) => {
  174. // Return a non-taskbar tab route immediately.
  175. if (!backRoute.taskbarTabEntityKey) return true
  176. // Ignore the current tab, we will be closing it shortly.
  177. if (backRoute.taskbarTabEntityKey === activeTaskbarTabEntityKey.value)
  178. return false
  179. // Check if the taskbar tab route is part of the current taskbar.
  180. return !!taskbarTabListByTabEntityKey.value[
  181. backRoute.taskbarTabEntityKey
  182. ]
  183. })
  184. // If identified, redirect to the historical route.
  185. if (nextRoute) {
  186. router.push(nextRoute.path)
  187. return
  188. }
  189. // Otherwise, redirect to the fallback route.
  190. // TODO: Adjust the following redirect fallback to Overviews page instead, when ready.
  191. router.push('/')
  192. }
  193. const handleActiveTaskbarTabRemoval = (
  194. taskbarTabList: UserCurrentTaskbarItemListQuery['userCurrentTaskbarItemList'],
  195. removedItemId: string,
  196. ) => {
  197. const removedItem = taskbarTabList?.find(
  198. (tab) => tab.id === removedItemId,
  199. )
  200. if (!removedItem) return
  201. if (removedItem.key !== activeTaskbarTabEntityKey.value) return
  202. // If the active taskbar tab was removed, redirect to the last historical route.
  203. redirectToLastHistoricalRoute()
  204. }
  205. taskbarTabsQuery.subscribeToMore<
  206. UserCurrentTaskbarItemUpdatesSubscriptionVariables,
  207. UserCurrentTaskbarItemUpdatesSubscription
  208. >({
  209. document: UserCurrentTaskbarItemUpdatesDocument,
  210. variables: {
  211. app: EnumTaskbarApp.Desktop,
  212. },
  213. updateQuery(previous, { subscriptionData }) {
  214. const updates = subscriptionData.data.userCurrentTaskbarItemUpdates
  215. if (!updates.addItem && !updates.updateItem && !updates.removeItem)
  216. return null as unknown as UserCurrentTaskbarItemListQuery
  217. if (!previous.userCurrentTaskbarItemList || updates.updateItem)
  218. return previous
  219. const previousTaskbarTabList = previous.userCurrentTaskbarItemList
  220. if (updates.removeItem) {
  221. const newTaskbarTabList = previousTaskbarTabList.filter(
  222. (tab) => tab.id !== updates.removeItem,
  223. )
  224. handleActiveTaskbarTabRemoval(
  225. previousTaskbarTabList,
  226. updates.removeItem,
  227. )
  228. return {
  229. userCurrentTaskbarItemList: newTaskbarTabList,
  230. }
  231. }
  232. if (updates.addItem) {
  233. const newIdPresent = previousTaskbarTabList.find((taskbarTab) => {
  234. return taskbarTab.id === updates.addItem?.id
  235. })
  236. if (newIdPresent) return previous
  237. return {
  238. userCurrentTaskbarItemList: [
  239. ...previousTaskbarTabList,
  240. updates.addItem,
  241. ],
  242. }
  243. }
  244. return previous
  245. },
  246. })
  247. taskbarTabsQuery.subscribeToMore<
  248. UserCurrentTaskbarItemListUpdatesSubscriptionVariables,
  249. UserCurrentTaskbarItemListUpdatesSubscription
  250. >({
  251. document: UserCurrentTaskbarItemListUpdatesDocument,
  252. variables: {
  253. app: EnumTaskbarApp.Desktop,
  254. },
  255. })
  256. const taskbarAddMutation = new MutationHandler(
  257. useUserCurrentTaskbarItemAddMutation({
  258. update: (cache, { data }) => {
  259. if (!data) return
  260. const { userCurrentTaskbarItemAdd } = data
  261. if (!userCurrentTaskbarItemAdd?.taskbarItem) return
  262. const newIdPresent = taskbarTabList.value.find((taskbarTab) => {
  263. return (
  264. taskbarTab.taskbarTabId ===
  265. userCurrentTaskbarItemAdd.taskbarItem?.id
  266. )
  267. })
  268. if (newIdPresent) return
  269. let existingTaskbarItemList =
  270. cache.readQuery<UserCurrentTaskbarItemListQuery>({
  271. query: UserCurrentTaskbarItemListDocument,
  272. })
  273. existingTaskbarItemList = {
  274. ...existingTaskbarItemList,
  275. userCurrentTaskbarItemList: [
  276. ...(existingTaskbarItemList?.userCurrentTaskbarItemList || []),
  277. userCurrentTaskbarItemAdd.taskbarItem,
  278. ],
  279. }
  280. cache.writeQuery({
  281. query: UserCurrentTaskbarItemListDocument,
  282. data: existingTaskbarItemList,
  283. })
  284. },
  285. }),
  286. )
  287. const addTaskbarTab = async (
  288. taskbarTabEntity: EnumTaskbarEntity,
  289. tabEntityKey: string,
  290. tabEntityInternalId: string,
  291. ) => {
  292. const { buildTaskbarTabParams, entityType, entityDocument } =
  293. getTaskbarTabTypePlugin(taskbarTabEntity)
  294. const order = hasTaskbarTabs.value
  295. ? taskbarTabList.value[taskbarTabList.value.length - 1].order + 1
  296. : 1
  297. // Add temporary in creation taskbar tab item when we have already an existing entity from the cache.
  298. if (entityType && entityDocument) {
  299. const cachedEntity = getApolloClient().cache.readFragment<ObjectWithId>(
  300. {
  301. id: `${entityType}:${convertToGraphQLId(
  302. entityType,
  303. tabEntityInternalId,
  304. )}`,
  305. fragment: entityDocument,
  306. },
  307. )
  308. if (cachedEntity) {
  309. taskbarTabsInCreation.value.push({
  310. type: taskbarTabEntity,
  311. entity: cachedEntity,
  312. tabEntityKey,
  313. order,
  314. })
  315. }
  316. }
  317. await taskbarAddMutation
  318. .send({
  319. input: {
  320. app: EnumTaskbarApp.Desktop,
  321. callback: taskbarTabEntity,
  322. key: tabEntityKey,
  323. notify: false,
  324. params: buildTaskbarTabParams(tabEntityInternalId),
  325. prio: order,
  326. },
  327. })
  328. .finally(() => {
  329. // Remove temporary in creation taskar tab again.
  330. taskbarTabsInCreation.value = taskbarTabsInCreation.value.filter(
  331. (tab) => tab.tabEntityKey !== tabEntityKey,
  332. )
  333. })
  334. }
  335. const taskbarUpdateMutation = new MutationHandler(
  336. useUserCurrentTaskbarItemUpdateMutation(),
  337. )
  338. const updateTaskbarTab = (taskbarTabId: ID, taskbarTab: UserTaskbarTab) => {
  339. taskbarUpdateMutation.send({
  340. id: taskbarTabId,
  341. input: {
  342. app: EnumTaskbarApp.Desktop,
  343. callback: taskbarTab.type,
  344. key: taskbarTab.tabEntityKey,
  345. notify: !!taskbarTab.notify,
  346. prio: taskbarTab.order,
  347. dirty: taskbarTab.dirty,
  348. },
  349. })
  350. }
  351. const taskbarTouchMutation = new MutationHandler(
  352. useUserCurrentTaskbarItemTouchLastContactMutation(),
  353. )
  354. const touchTaskbarTab = async (taskbarTabId: ID) => {
  355. await taskbarTouchMutation.send({
  356. id: taskbarTabId,
  357. })
  358. }
  359. const upsertTaskbarTab = async (
  360. taskbarTabEntity: EnumTaskbarEntity,
  361. tabEntityKey: string,
  362. tabEntityInternalId: string,
  363. ) => {
  364. activeTaskbarTabEntityKey.value = tabEntityKey
  365. if (!taskbarTabExists(taskbarTabEntity, tabEntityKey)) {
  366. await addTaskbarTab(taskbarTabEntity, tabEntityKey, tabEntityInternalId)
  367. }
  368. const taskbarTab = taskbarTabListByTabEntityKey.value[tabEntityKey]
  369. if (!taskbarTab || !taskbarTab.taskbarTabId) return
  370. await touchTaskbarTab(taskbarTab.taskbarTabId)
  371. }
  372. const resetActiveTaskbarTab = () => {
  373. if (!activeTaskbarTabEntityKey.value) return
  374. activeTaskbarTabEntityKey.value = undefined
  375. }
  376. let silenceTaskbarDeleteError = false
  377. const taskbarDeleteMutation = new MutationHandler(
  378. useUserCurrentTaskbarItemDeleteMutation(),
  379. {
  380. errorCallback: () => {
  381. if (silenceTaskbarDeleteError) return false
  382. },
  383. },
  384. )
  385. const deleteTaskbarTab = (taskbarTabId: ID, silenceError?: boolean) => {
  386. taskbarTabIDsInDeletion.value.push(taskbarTabId)
  387. if (silenceError) silenceTaskbarDeleteError = true
  388. taskbarDeleteMutation
  389. .send({
  390. id: taskbarTabId,
  391. })
  392. .catch(() => {
  393. taskbarTabIDsInDeletion.value = taskbarTabIDsInDeletion.value.filter(
  394. (inDeletionTaskbarTabId) => inDeletionTaskbarTabId !== taskbarTabId,
  395. )
  396. })
  397. .finally(() => {
  398. if (silenceError) silenceTaskbarDeleteError = false
  399. })
  400. }
  401. watch(taskbarTabList, (newTaskbarTabList) => {
  402. if (
  403. !newTaskbarTabList ||
  404. newTaskbarTabList.length <=
  405. application.config.ui_task_mananger_max_task_count
  406. )
  407. return
  408. const sortedTaskbarTabList = newTaskbarTabList
  409. .filter(
  410. (taskbarTab) =>
  411. taskbarTab.taskbarTabId !== activeTaskbarTab.value?.taskbarTabId &&
  412. taskbarTab.updatedAt &&
  413. !taskbarTab.changed &&
  414. !taskbarTab.dirty,
  415. )
  416. .sort(
  417. (a, b) =>
  418. new Date(a.updatedAt!).getTime() - new Date(b.updatedAt!).getTime(),
  419. )
  420. if (!sortedTaskbarTabList.length) return
  421. const oldestTaskbarTab = sortedTaskbarTabList.at(0)
  422. if (!oldestTaskbarTab?.taskbarTabId) return
  423. log.info(
  424. `More than the allowed maximum number of tasks are open (${application.config.ui_task_mananger_max_task_count}), closing the oldest untouched task now.`,
  425. oldestTaskbarTab.tabEntityKey,
  426. )
  427. deleteTaskbarTab(oldestTaskbarTab.taskbarTabId, true)
  428. })
  429. const waitForTaskbarListLoaded = () => {
  430. return new Promise<void>((resolve) => {
  431. const interval = setInterval(() => {
  432. if (taskbarTabsLoading.value) return
  433. clearInterval(interval)
  434. resolve()
  435. })
  436. })
  437. }
  438. tryOnScopeDispose(() => {
  439. taskbarTabsQuery.stop()
  440. })
  441. return {
  442. taskbarTabIDsInDeletion,
  443. activeTaskbarTabId,
  444. activeTaskbarTab,
  445. activeTaskbarTabEntityKey,
  446. activeTaskbarTabEntityAccess,
  447. taskbarTabContexts,
  448. taskbarTabsInCreation,
  449. taskbarTabsRaw,
  450. taskbarTabList,
  451. taskbarTabListByTabEntityKey,
  452. taskbarLookupByTypeAndTabEntityKey,
  453. taskbarTabListOrder,
  454. taskbarTabExists,
  455. getTaskbarTabTypePlugin,
  456. addTaskbarTab,
  457. updateTaskbarTab,
  458. upsertTaskbarTab,
  459. deleteTaskbarTab,
  460. resetActiveTaskbarTab,
  461. waitForTaskbarListLoaded,
  462. loading: taskbarTabsLoading,
  463. hasTaskbarTabs,
  464. }
  465. },
  466. )
  467. if (import.meta.hot) {
  468. import.meta.hot.accept(
  469. acceptHMRUpdate(useUserCurrentTaskbarTabsStore, import.meta.hot),
  470. )
  471. }