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