UserTaskbarTabs.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { animations, parents } from '@formkit/drag-and-drop'
  4. import { dragAndDrop } from '@formkit/drag-and-drop/vue'
  5. import { cloneDeep } from 'lodash-es'
  6. import { storeToRefs } from 'pinia'
  7. import { type Ref, ref, watch, useTemplateRef } from 'vue'
  8. import CommonPopover from '#shared/components/CommonPopover/CommonPopover.vue'
  9. import { usePopover } from '#shared/components/CommonPopover/usePopover.ts'
  10. import { EnumTaskbarEntityAccess } from '#shared/graphql/types.ts'
  11. import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
  12. import { startAndEndEventsDNDPlugin } from '#shared/utils/startAndEndEventsDNDPlugin.ts'
  13. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  14. import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
  15. import CommonSectionCollapse from '#desktop/components/CommonSectionCollapse/CommonSectionCollapse.vue'
  16. import { useUserCurrentTaskbarItemListPrioMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentTaskbarItemListPrio.api.ts'
  17. import { useUserCurrentTaskbarTabsStore } from '#desktop/entities/user/current/stores/taskbarTabs.ts'
  18. import UserTaskbarTabForbidden from './UserTaskbarTabForbidden.vue'
  19. import UserTaskbarTabNotFound from './UserTaskbarTabNotFound.vue'
  20. import UserTaskbarTabRemove from './UserTaskbarTabRemove.vue'
  21. export interface Props {
  22. collapsed?: boolean
  23. }
  24. const props = defineProps<Props>()
  25. const taskbarTabStore = useUserCurrentTaskbarTabsStore()
  26. const {
  27. taskbarTabList,
  28. taskbarTabListByTabEntityKey,
  29. taskbarTabListOrder,
  30. hasTaskbarTabs,
  31. activeTaskbarTabEntityKey,
  32. activeTaskbarTabContext,
  33. loading,
  34. } = storeToRefs(taskbarTabStore)
  35. const { getTaskbarTabTypePlugin } = taskbarTabStore
  36. const dndStartCallback = (parent: HTMLElement) => {
  37. const siblings = parent.querySelectorAll('.draggable:not(.dragging-active)')
  38. // Temporarily suspend tab hover states.
  39. siblings.forEach((sibling) => {
  40. sibling.classList.remove('group/tab')
  41. sibling.classList.add('no-tooltip')
  42. })
  43. }
  44. const userCurrentTaskbarItemListPrioMutation = new MutationHandler(
  45. useUserCurrentTaskbarItemListPrioMutation(),
  46. )
  47. const updateTaskbarTabListOrder = (newTaskbarTabListOrder: string[]) => {
  48. const taskbarTabListPrio = newTaskbarTabListOrder
  49. ?.map((tabEntityKey, index) => ({
  50. id: taskbarTabListByTabEntityKey.value[tabEntityKey].taskbarTabId!,
  51. prio: index + 1,
  52. }))
  53. .filter((taskbarTabListPrioItem) => taskbarTabListPrioItem.id)
  54. if (!taskbarTabListPrio?.length) return
  55. userCurrentTaskbarItemListPrioMutation.send({
  56. list: taskbarTabListPrio,
  57. })
  58. }
  59. const dndEndCallback = (parent: HTMLElement) => {
  60. const parentData = parents.get(parent)
  61. if (parentData) {
  62. updateTaskbarTabListOrder(parentData.getValues(parent))
  63. }
  64. const siblings = parent.querySelectorAll('.draggable:not(.dragging-active)')
  65. // Reactivate tab hover states.
  66. siblings.forEach((sibling) => {
  67. sibling.classList.add('group/tab')
  68. sibling.classList.remove('no-tooltip')
  69. })
  70. // NB: Workaround for a Chrome bug where the hover state may get stuck once drag is over.
  71. // https://issues.chromium.org/issues/41129937#comment6
  72. setTimeout(() => {
  73. parent.classList.add('pointer-events-none')
  74. requestAnimationFrame(() => {
  75. parent.classList.remove('pointer-events-none')
  76. })
  77. }, 0)
  78. }
  79. const dndParentElement = useTemplateRef('dnd-parent')
  80. const dndTaskbarTabListOrder = ref(taskbarTabListOrder.value || [])
  81. watch(taskbarTabListOrder, (newValue) => {
  82. dndTaskbarTabListOrder.value = cloneDeep(newValue || [])
  83. })
  84. dragAndDrop({
  85. parent: dndParentElement as Ref<HTMLElement>,
  86. values: dndTaskbarTabListOrder,
  87. plugins: [
  88. startAndEndEventsDNDPlugin(dndStartCallback, dndEndCallback),
  89. animations(),
  90. ],
  91. dropZoneClass: 'opacity-0 no-tooltip dragging-active',
  92. touchDropZoneClass: 'opacity-0 no-tooltip dragging-active',
  93. draggingClass: 'dragging-active',
  94. })
  95. const getTaskbarTabComponent = (tabEntityKey: string) => {
  96. const taskbarTab = taskbarTabListByTabEntityKey.value[tabEntityKey]
  97. if (!taskbarTab) return
  98. if (
  99. !taskbarTab.entityAccess ||
  100. taskbarTab.entityAccess === EnumTaskbarEntityAccess.Granted
  101. )
  102. return getTaskbarTabTypePlugin(taskbarTab.type).component
  103. if (taskbarTab.entityAccess === EnumTaskbarEntityAccess.Forbidden)
  104. return UserTaskbarTabForbidden
  105. if (taskbarTab.entityAccess === EnumTaskbarEntityAccess.NotFound)
  106. return UserTaskbarTabNotFound
  107. }
  108. const getTaskbarTabLink = (tabEntityKey: string) => {
  109. const taskbarTab = taskbarTabListByTabEntityKey.value[tabEntityKey]
  110. if (!taskbarTab) return
  111. if (
  112. taskbarTab.entityAccess === EnumTaskbarEntityAccess.Forbidden ||
  113. taskbarTab.entityAccess === EnumTaskbarEntityAccess.NotFound
  114. )
  115. return
  116. const plugin = getTaskbarTabTypePlugin(taskbarTab.type)
  117. if (typeof plugin.buildTaskbarTabLink !== 'function') return
  118. return plugin.buildTaskbarTabLink(taskbarTab.entity) ?? '#'
  119. }
  120. const { popover, popoverTarget, toggle, isOpen: popoverIsOpen } = usePopover()
  121. </script>
  122. <template>
  123. <CommonLoader :loading="loading">
  124. <div
  125. v-if="hasTaskbarTabs"
  126. class="-m-1 flex flex-col overflow-y-hidden py-2"
  127. >
  128. <div v-if="props.collapsed" class="flex justify-center">
  129. <CommonPopover
  130. id="user-taskbar-tabs-popover"
  131. ref="popover"
  132. class="min-w-52 max-w-64"
  133. :owner="popoverTarget"
  134. orientation="autoHorizontal"
  135. placement="start"
  136. hide-arrow
  137. persistent
  138. >
  139. <ul>
  140. <li
  141. v-for="userTaskbarTab in taskbarTabList"
  142. :key="userTaskbarTab.tabEntityKey"
  143. class="group/tab relative"
  144. >
  145. <component
  146. :is="getTaskbarTabComponent(userTaskbarTab.tabEntityKey)"
  147. :entity="userTaskbarTab.entity"
  148. :context="
  149. activeTaskbarTabEntityKey === userTaskbarTab.tabEntityKey
  150. ? activeTaskbarTabContext
  151. : undefined
  152. "
  153. :taskbar-tab="userTaskbarTab"
  154. :taskbar-tab-link="
  155. getTaskbarTabLink(userTaskbarTab.tabEntityKey)
  156. "
  157. class="group/link rounded-none focus-visible:bg-blue-800 focus-visible:outline-0 group-first/tab:rounded-t-[10px] group-last/tab:rounded-b-[10px]"
  158. />
  159. <UserTaskbarTabRemove
  160. :taskbar-tab-id="userTaskbarTab.taskbarTabId"
  161. :dirty="
  162. activeTaskbarTabEntityKey === userTaskbarTab.tabEntityKey
  163. ? (activeTaskbarTabContext.formIsDirty ??
  164. userTaskbarTab.dirty)
  165. : userTaskbarTab.dirty
  166. "
  167. :plugin="getTaskbarTabTypePlugin(userTaskbarTab.type)"
  168. />
  169. </li>
  170. </ul>
  171. </CommonPopover>
  172. <CommonButton
  173. id="user-taskbar-tabs-popover-button"
  174. ref="popoverTarget"
  175. class="text-neutral-400 hover:outline-blue-900"
  176. icon="card-list"
  177. size="medium"
  178. variant="neutral"
  179. :aria-controls="
  180. popoverIsOpen ? 'user-taskbar-tabs-popover' : undefined
  181. "
  182. aria-haspopup="true"
  183. :aria-expanded="popoverIsOpen"
  184. :aria-label="$t('List of all user taskbar tabs')"
  185. :class="{
  186. '!bg-blue-800 !text-white': popoverIsOpen,
  187. }"
  188. @click="toggle(true)"
  189. />
  190. </div>
  191. <template v-else>
  192. <CommonSectionCollapse
  193. id="user-taskbar-tabs"
  194. :title="__('Tabs')"
  195. no-negative-margin
  196. >
  197. <span id="drag-and-drop-taskbar-tabs" class="sr-only">
  198. {{ $t('Drag and drop to reorder your tabs.') }}
  199. </span>
  200. <ul
  201. ref="dnd-parent"
  202. class="flex flex-col gap-1.5 overflow-y-auto p-1"
  203. data-theme="dark"
  204. :style="{ colorScheme: 'dark' }"
  205. >
  206. <li
  207. v-for="tabEntityKey in dndTaskbarTabListOrder"
  208. :key="tabEntityKey"
  209. class="draggable group/tab relative"
  210. draggable="true"
  211. aria-describedby="drag-and-drop-taskbar-tabs"
  212. >
  213. <component
  214. :is="getTaskbarTabComponent(tabEntityKey)"
  215. :entity="taskbarTabListByTabEntityKey[tabEntityKey].entity"
  216. :context="
  217. activeTaskbarTabEntityKey === tabEntityKey
  218. ? activeTaskbarTabContext
  219. : undefined
  220. "
  221. :taskbar-tab="taskbarTabListByTabEntityKey[tabEntityKey]"
  222. :taskbar-tab-link="getTaskbarTabLink(tabEntityKey)"
  223. class="active:cursor-grabbing"
  224. />
  225. <UserTaskbarTabRemove
  226. :taskbar-tab-id="
  227. taskbarTabListByTabEntityKey[tabEntityKey].taskbarTabId
  228. "
  229. :dirty="
  230. activeTaskbarTabEntityKey === tabEntityKey
  231. ? (activeTaskbarTabContext.formIsDirty ??
  232. taskbarTabListByTabEntityKey[tabEntityKey].dirty)
  233. : taskbarTabListByTabEntityKey[tabEntityKey].dirty
  234. "
  235. :plugin="
  236. getTaskbarTabTypePlugin(
  237. taskbarTabListByTabEntityKey[tabEntityKey].type,
  238. )
  239. "
  240. />
  241. </li>
  242. </ul>
  243. </CommonSectionCollapse>
  244. </template>
  245. </div>
  246. </CommonLoader>
  247. </template>