taskbarTabs.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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. 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 hasTaskbarTabs = computed(() => taskbarTabList.value?.length > 0)
  190. const taskbarTabListByTabEntityKey = computed(() =>
  191. keyBy(taskbarTabList.value, 'tabEntityKey'),
  192. )
  193. const taskbarTabListOrder = computed(() =>
  194. taskbarTabList.value.reduce((taskbarTabListOrder, taskbarTab) => {
  195. taskbarTabListOrder.push(taskbarTab.tabEntityKey)
  196. return taskbarTabListOrder
  197. }, [] as string[]),
  198. )
  199. const taskbarLookupByTypeAndTabEntityKey = computed(() => {
  200. return taskbarTabList.value.reduce(
  201. (lookup, tab) => {
  202. if (!tab.taskbarTabId) return lookup
  203. lookup[tab.type] = lookup[tab.type] || {}
  204. lookup[tab.type][tab.tabEntityKey] = tab.taskbarTabId
  205. return lookup
  206. },
  207. {} as Record<EnumTaskbarEntity, Record<ID, ID>>,
  208. )
  209. })
  210. const taskbarTabExists = (type: EnumTaskbarEntity, tabEntityKey: string) =>
  211. Boolean(taskbarLookupByTypeAndTabEntityKey.value[type]?.[tabEntityKey])
  212. const taskbarAddMutation = new MutationHandler(
  213. useUserCurrentTaskbarItemAddMutation({
  214. update: (cache, { data }) => {
  215. if (!data) return
  216. const { userCurrentTaskbarItemAdd } = data
  217. if (!userCurrentTaskbarItemAdd?.taskbarItem) return
  218. const newIdPresent = taskbarTabList.value.find((taskbarTab) => {
  219. return (
  220. taskbarTab.taskbarTabId ===
  221. userCurrentTaskbarItemAdd.taskbarItem?.id
  222. )
  223. })
  224. if (newIdPresent) return
  225. let existingTaskbarItemList =
  226. cache.readQuery<UserCurrentTaskbarItemListQuery>({
  227. query: UserCurrentTaskbarItemListDocument,
  228. })
  229. existingTaskbarItemList = {
  230. ...existingTaskbarItemList,
  231. userCurrentTaskbarItemList: [
  232. ...(existingTaskbarItemList?.userCurrentTaskbarItemList || []),
  233. userCurrentTaskbarItemAdd.taskbarItem,
  234. ],
  235. }
  236. cache.writeQuery({
  237. query: UserCurrentTaskbarItemListDocument,
  238. data: existingTaskbarItemList,
  239. })
  240. },
  241. }),
  242. )
  243. const addTaskbarTab = (
  244. taskbarTabEntity: EnumTaskbarEntity,
  245. tabEntityKey: string,
  246. tabEntityInternalId: string,
  247. ) => {
  248. const { buildTaskbarTabParams, entityType, entityDocument } =
  249. getTaskbarTabTypePlugin(taskbarTabEntity)
  250. const order = hasTaskbarTabs.value
  251. ? taskbarTabList.value[taskbarTabList.value.length - 1].order + 1
  252. : 1
  253. // Add temporary in creation taskbar tab item when we have already an existing entity from the cache.
  254. if (entityType && entityDocument) {
  255. const cachedEntity = getApolloClient().cache.readFragment<ObjectWithId>(
  256. {
  257. id: `${entityType}:${convertToGraphQLId(
  258. entityType,
  259. tabEntityInternalId,
  260. )}`,
  261. fragment: entityDocument,
  262. },
  263. )
  264. if (cachedEntity) {
  265. taskbarTabsInCreation.value.push({
  266. type: taskbarTabEntity,
  267. entity: cachedEntity,
  268. tabEntityKey,
  269. lastContact: new Date().toISOString(),
  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 ?? false,
  328. prio: taskbarTab.order,
  329. dirty: taskbarTab.dirty,
  330. },
  331. })
  332. }
  333. const upsertTaskbarTab = (
  334. taskbarTabEntity: EnumTaskbarEntity,
  335. tabEntityKey: string,
  336. tabEntityInternalId: string,
  337. ) => {
  338. activeTaskbarTabEntityKey.value = tabEntityKey
  339. if (!taskbarTabExists(taskbarTabEntity, tabEntityKey)) {
  340. addTaskbarTab(taskbarTabEntity, tabEntityKey, tabEntityInternalId)
  341. return
  342. }
  343. // TODO: Do something for existing tabs here???
  344. console.log('HERE-SOMETHING-FOR-EXISTING-TABS')
  345. }
  346. const resetActiveTaskbarTab = () => {
  347. activeTaskbarTabEntityKey.value = undefined
  348. }
  349. let silenceTaskbarDeleteError = false
  350. const taskbarDeleteMutation = new MutationHandler(
  351. useUserCurrentTaskbarItemDeleteMutation(),
  352. {
  353. errorCallback: () => {
  354. if (silenceTaskbarDeleteError) return false
  355. },
  356. },
  357. )
  358. const deleteTaskbarTab = (taskbarTabId: ID, silenceError?: boolean) => {
  359. taskbarTabIDsInDeletion.value.push(taskbarTabId)
  360. if (silenceError) silenceTaskbarDeleteError = true
  361. taskbarDeleteMutation
  362. .send({
  363. id: taskbarTabId,
  364. })
  365. .catch(() => {
  366. taskbarTabIDsInDeletion.value = taskbarTabIDsInDeletion.value.filter(
  367. (inDeletionTaskbarTabId) => inDeletionTaskbarTabId !== taskbarTabId,
  368. )
  369. })
  370. .finally(() => {
  371. if (silenceError) silenceTaskbarDeleteError = false
  372. })
  373. }
  374. watch(taskbarTabList, (newTaskbarTabList) => {
  375. if (
  376. !newTaskbarTabList ||
  377. newTaskbarTabList.length <=
  378. application.config.ui_task_mananger_max_task_count
  379. )
  380. return
  381. const sortedTaskbarTabList = newTaskbarTabList
  382. .filter(
  383. (taskbarTab) =>
  384. taskbarTab.taskbarTabId !== activeTaskbarTab.value?.taskbarTabId &&
  385. taskbarTab.updatedAt &&
  386. !taskbarTab.changed &&
  387. !taskbarTab.dirty,
  388. )
  389. .sort(
  390. (a, b) =>
  391. new Date(a.updatedAt!).getTime() - new Date(b.updatedAt!).getTime(),
  392. )
  393. if (!sortedTaskbarTabList.length) return
  394. const oldestTaskbarTab = sortedTaskbarTabList.at(0)
  395. if (!oldestTaskbarTab?.taskbarTabId) return
  396. log.info(
  397. `More than the allowed maximum number of tasks are open (${application.config.ui_task_mananger_max_task_count}), closing the oldest untouched task now.`,
  398. oldestTaskbarTab.tabEntityKey,
  399. )
  400. deleteTaskbarTab(oldestTaskbarTab.taskbarTabId, true)
  401. })
  402. const waitForTaskbarListLoaded = () => {
  403. return new Promise<void>((resolve) => {
  404. const interval = setInterval(() => {
  405. if (taskbarTabsLoading.value) return
  406. clearInterval(interval)
  407. resolve()
  408. })
  409. })
  410. }
  411. tryOnScopeDispose(() => {
  412. taskbarTabsQuery.stop()
  413. })
  414. return {
  415. taskbarTabIDsInDeletion,
  416. activeTaskbarTabId,
  417. activeTaskbarTab,
  418. activeTaskbarTabEntityKey,
  419. activeTaskbarTabContext,
  420. taskbarTabsInCreation,
  421. taskbarTabsRaw,
  422. taskbarTabList,
  423. taskbarTabListByTabEntityKey,
  424. taskbarTabListOrder,
  425. taskbarTabExists,
  426. getTaskbarTabTypePlugin,
  427. addTaskbarTab,
  428. updateTaskbarTab,
  429. upsertTaskbarTab,
  430. deleteTaskbarTab,
  431. resetActiveTaskbarTab,
  432. waitForTaskbarListLoaded,
  433. loading: taskbarTabsLoading,
  434. hasTaskbarTabs,
  435. }
  436. },
  437. )
  438. if (import.meta.hot) {
  439. import.meta.hot.accept(
  440. acceptHMRUpdate(useUserCurrentTaskbarTabsStore, import.meta.hot),
  441. )
  442. }