UserTaskbarTabs.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { animations, parents, updateConfig } from '@formkit/drag-and-drop'
  4. import { dragAndDrop } from '@formkit/drag-and-drop/vue'
  5. import { computedAsync } from '@vueuse/core'
  6. import { cloneDeep } from 'lodash-es'
  7. import { storeToRefs } from 'pinia'
  8. import { type Ref, ref, watch, useTemplateRef, nextTick } from 'vue'
  9. import CommonPopover from '#shared/components/CommonPopover/CommonPopover.vue'
  10. import { usePopover } from '#shared/components/CommonPopover/usePopover.ts'
  11. import { EnumTaskbarEntityAccess } from '#shared/graphql/types.ts'
  12. import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
  13. import { startAndEndEventsDNDPlugin } from '#shared/utils/startAndEndEventsDNDPlugin.ts'
  14. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  15. import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
  16. import CommonSectionCollapse from '#desktop/components/CommonSectionCollapse/CommonSectionCollapse.vue'
  17. import { useUserCurrentTaskbarItemListPrioMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentTaskbarItemListPrio.api.ts'
  18. import { useUserCurrentTaskbarTabsStore } from '#desktop/entities/user/current/stores/taskbarTabs.ts'
  19. import UserTaskbarTabForbidden from './UserTaskbarTabForbidden.vue'
  20. import UserTaskbarTabNotFound from './UserTaskbarTabNotFound.vue'
  21. import UserTaskbarTabRemove from './UserTaskbarTabRemove.vue'
  22. export interface Props {
  23. collapsed?: boolean
  24. }
  25. const props = defineProps<Props>()
  26. const taskbarTabStore = useUserCurrentTaskbarTabsStore()
  27. const {
  28. taskbarTabListByTabEntityKey,
  29. taskbarTabListOrder,
  30. hasTaskbarTabs,
  31. taskbarTabContexts,
  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 dndParentElement = useTemplateRef('dnd-parent')
  79. const dndTaskbarTabListOrder = ref(taskbarTabListOrder.value || [])
  80. watch(taskbarTabListOrder, (newValue) => {
  81. dndTaskbarTabListOrder.value = cloneDeep(newValue || [])
  82. })
  83. dragAndDrop({
  84. parent: dndParentElement as Ref<HTMLElement>,
  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. watch(
  95. () => props.collapsed,
  96. (isCollapsed) => {
  97. if (!dndParentElement.value) return
  98. updateConfig(dndParentElement.value, { disabled: isCollapsed })
  99. },
  100. )
  101. const getTaskbarTabComponent = (tabEntityKey: string) => {
  102. const taskbarTab = taskbarTabListByTabEntityKey.value[tabEntityKey]
  103. if (!taskbarTab) return
  104. if (
  105. !taskbarTab.entityAccess ||
  106. taskbarTab.entityAccess === EnumTaskbarEntityAccess.Granted
  107. )
  108. return getTaskbarTabTypePlugin(taskbarTab.type).component
  109. if (taskbarTab.entityAccess === EnumTaskbarEntityAccess.Forbidden)
  110. return UserTaskbarTabForbidden
  111. if (taskbarTab.entityAccess === EnumTaskbarEntityAccess.NotFound)
  112. return UserTaskbarTabNotFound
  113. }
  114. const getTaskbarTabLink = (tabEntityKey: string) => {
  115. const taskbarTab = taskbarTabListByTabEntityKey.value[tabEntityKey]
  116. if (!taskbarTab) return
  117. const plugin = getTaskbarTabTypePlugin(taskbarTab.type)
  118. if (typeof plugin.buildTaskbarTabLink !== 'function') return
  119. return (
  120. plugin.buildTaskbarTabLink(taskbarTab.entity, taskbarTab.tabEntityKey) ??
  121. '#'
  122. )
  123. }
  124. const { popover, popoverTarget, toggle, isOpen: popoverIsOpen } = usePopover()
  125. const taskbarTabListContainer = useTemplateRef('taskbar-tab-list')
  126. const taskbarTabListLocation = computedAsync(() => {
  127. if (!taskbarTabListContainer.value) return '#taskbarTabListHidden'
  128. // NB: Prevent teleport component from complaining that the target is not ready.
  129. // Defer the value update for after next tick.
  130. return nextTick(() => {
  131. if (props.collapsed) return '#taskbarTabListCollapsed'
  132. return '#taskbarTabListExpanded'
  133. })
  134. }, '#taskbarTabListHidden')
  135. const getTaskbarTabContext = (tabEntityKey: string) => {
  136. if (!taskbarTabListContainer.value) return
  137. return taskbarTabContexts.value[tabEntityKey]
  138. }
  139. const getTaskbarTabDirtyFlag = (tabEntityKey: string) => {
  140. if (!taskbarTabListContainer.value) return
  141. return (
  142. taskbarTabContexts.value[tabEntityKey]?.formIsDirty ??
  143. taskbarTabListByTabEntityKey.value[tabEntityKey].dirty
  144. )
  145. }
  146. </script>
  147. <template>
  148. <CommonLoader :loading="loading">
  149. <div
  150. v-if="hasTaskbarTabs"
  151. class="-m-1 flex flex-col overflow-y-hidden py-2"
  152. >
  153. <div v-if="props.collapsed" class="flex justify-center">
  154. <CommonPopover
  155. id="user-taskbar-tabs-popover"
  156. ref="popover"
  157. class="min-w-52 max-w-64"
  158. :owner="popoverTarget"
  159. orientation="autoHorizontal"
  160. placement="start"
  161. hide-arrow
  162. persistent
  163. >
  164. <div id="taskbarTabListCollapsed" ref="taskbar-tab-list" />
  165. </CommonPopover>
  166. <CommonButton
  167. id="user-taskbar-tabs-popover-button"
  168. ref="popoverTarget"
  169. class="text-neutral-400 hover:outline-blue-900"
  170. icon="card-list"
  171. size="large"
  172. variant="neutral"
  173. :aria-controls="
  174. popoverIsOpen ? 'user-taskbar-tabs-popover' : undefined
  175. "
  176. aria-haspopup="true"
  177. :aria-expanded="popoverIsOpen"
  178. :aria-label="$t('List of all user taskbar tabs')"
  179. :class="{
  180. '!bg-blue-800 !text-white': popoverIsOpen,
  181. }"
  182. @click="toggle(true)"
  183. />
  184. </div>
  185. <template v-else>
  186. <CommonSectionCollapse
  187. id="user-taskbar-tabs"
  188. :title="__('Tabs')"
  189. no-negative-margin
  190. scrollable
  191. >
  192. <span id="drag-and-drop-taskbar-tabs" class="sr-only">
  193. {{ $t('Drag and drop to reorder your tabs.') }}
  194. </span>
  195. <div id="taskbarTabListExpanded" ref="taskbar-tab-list" />
  196. </CommonSectionCollapse>
  197. </template>
  198. <div id="taskbarTabListHidden" class="hidden" aria-hidden="true">
  199. <Teleport :to="taskbarTabListLocation" defer>
  200. <ul
  201. ref="dnd-parent"
  202. :class="{ 'flex flex-col gap-1.5 overflow-y-auto p-1': !collapsed }"
  203. >
  204. <li
  205. v-for="tabEntityKey in dndTaskbarTabListOrder"
  206. :key="tabEntityKey"
  207. class="group/tab relative"
  208. :class="{ draggable: !collapsed }"
  209. :draggable="!collapsed ? 'true' : undefined"
  210. :aria-describedby="
  211. !collapsed ? 'drag-and-drop-taskbar-tabs' : undefined
  212. "
  213. >
  214. <component
  215. :is="getTaskbarTabComponent(tabEntityKey)"
  216. :context="getTaskbarTabContext(tabEntityKey)"
  217. :taskbar-tab="taskbarTabListByTabEntityKey[tabEntityKey]"
  218. :taskbar-tab-link="getTaskbarTabLink(tabEntityKey)"
  219. :class="{
  220. '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]':
  221. collapsed,
  222. 'active:cursor-grabbing': !collapsed,
  223. }"
  224. />
  225. <UserTaskbarTabRemove
  226. :taskbar-tab="taskbarTabListByTabEntityKey[tabEntityKey]"
  227. :dirty="getTaskbarTabDirtyFlag(tabEntityKey)"
  228. :plugin="
  229. getTaskbarTabTypePlugin(
  230. taskbarTabListByTabEntityKey[tabEntityKey].type,
  231. )
  232. "
  233. />
  234. </li>
  235. </ul>
  236. </Teleport>
  237. </div>
  238. </div>
  239. </CommonLoader>
  240. </template>