taskbarTabs.ts 18 KB

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