taskbarTabs.ts 17 KB

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