taskbarTabs.ts 16 KB

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