taskbarTabs.ts 16 KB

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