UserTaskbarTabs.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. 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. watch(
  96. () => props.collapsed,
  97. (isCollapsed) => {
  98. if (!dndParentElement.value) return
  99. updateConfig(dndParentElement.value, { disabled: isCollapsed })
  100. },
  101. )
  102. const getTaskbarTabComponent = (tabEntityKey: string) => {
  103. const taskbarTab = taskbarTabListByTabEntityKey.value[tabEntityKey]
  104. if (!taskbarTab) return
  105. if (
  106. !taskbarTab.entityAccess ||
  107. taskbarTab.entityAccess === EnumTaskbarEntityAccess.Granted
  108. )
  109. return getTaskbarTabTypePlugin(taskbarTab.type).component
  110. if (taskbarTab.entityAccess === EnumTaskbarEntityAccess.Forbidden)
  111. return UserTaskbarTabForbidden
  112. if (taskbarTab.entityAccess === EnumTaskbarEntityAccess.NotFound)
  113. return UserTaskbarTabNotFound
  114. }
  115. const getTaskbarTabLink = (tabEntityKey: string) => {
  116. const taskbarTab = taskbarTabListByTabEntityKey.value[tabEntityKey]
  117. if (!taskbarTab) return
  118. const plugin = getTaskbarTabTypePlugin(taskbarTab.type)
  119. if (typeof plugin.buildTaskbarTabLink !== 'function') return
  120. return (
  121. plugin.buildTaskbarTabLink(taskbarTab.entity, taskbarTab.tabEntityKey) ??
  122. '#'
  123. )
  124. }
  125. const { popover, popoverTarget, toggle, isOpen: popoverIsOpen } = usePopover()
  126. const taskbarTabListContainer = useTemplateRef('taskbar-tab-list')
  127. const taskbarTabListLocation = computedAsync(() => {
  128. if (!taskbarTabListContainer.value) return '#taskbarTabListHidden'
  129. // NB: Prevent teleport component from complaining that the target is not ready.
  130. // Defer the value update for after next tick.
  131. return nextTick(() => {
  132. if (props.collapsed) return '#taskbarTabListCollapsed'
  133. return '#taskbarTabListExpanded'
  134. })
  135. }, '#taskbarTabListHidden')
  136. const getTaskbarTabContext = (tabEntityKey: string) => {
  137. if (!taskbarTabListContainer.value) return
  138. return activeTaskbarTabEntityKey.value === tabEntityKey
  139. ? activeTaskbarTabContext.value
  140. : undefined
  141. }
  142. const getTaskbarTabDirtyFlag = (tabEntityKey: string) => {
  143. if (!taskbarTabListContainer.value) return
  144. if (activeTaskbarTabEntityKey.value === tabEntityKey)
  145. return (
  146. activeTaskbarTabContext.value.formIsDirty ??
  147. taskbarTabListByTabEntityKey.value[tabEntityKey].dirty
  148. )
  149. return taskbarTabListByTabEntityKey.value[tabEntityKey].dirty
  150. }
  151. </script>
  152. <template>
  153. <CommonLoader :loading="loading">
  154. <div
  155. v-if="hasTaskbarTabs"
  156. class="-m-1 flex flex-col overflow-y-hidden py-2"
  157. >
  158. <div v-if="props.collapsed" class="flex justify-center">
  159. <CommonPopover
  160. id="user-taskbar-tabs-popover"
  161. ref="popover"
  162. class="min-w-52 max-w-64"
  163. :owner="popoverTarget"
  164. orientation="autoHorizontal"
  165. placement="start"
  166. hide-arrow
  167. persistent
  168. >
  169. <div id="taskbarTabListCollapsed" ref="taskbar-tab-list" />
  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="large"
  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. <CommonSectionCollapse
  192. id="user-taskbar-tabs"
  193. :title="__('Tabs')"
  194. no-negative-margin
  195. scrollable
  196. >
  197. <span id="drag-and-drop-taskbar-tabs" class="sr-only">
  198. {{ $t('Drag and drop to reorder your tabs.') }}
  199. </span>
  200. <div id="taskbarTabListExpanded" ref="taskbar-tab-list" />
  201. </CommonSectionCollapse>
  202. </template>
  203. <div id="taskbarTabListHidden" class="hidden" aria-hidden="true">
  204. <Teleport :to="taskbarTabListLocation" defer>
  205. <ul
  206. ref="dnd-parent"
  207. :class="{ 'flex flex-col gap-1.5 overflow-y-auto p-1': !collapsed }"
  208. >
  209. <li
  210. v-for="tabEntityKey in dndTaskbarTabListOrder"
  211. :key="tabEntityKey"
  212. class="group/tab relative"
  213. :class="{ draggable: !collapsed }"
  214. :draggable="!collapsed ? 'true' : undefined"
  215. :aria-describedby="
  216. !collapsed ? 'drag-and-drop-taskbar-tabs' : undefined
  217. "
  218. >
  219. <component
  220. :is="getTaskbarTabComponent(tabEntityKey)"
  221. :entity="taskbarTabListByTabEntityKey[tabEntityKey].entity"
  222. :context="getTaskbarTabContext(tabEntityKey)"
  223. :taskbar-tab="taskbarTabListByTabEntityKey[tabEntityKey]"
  224. :taskbar-tab-link="getTaskbarTabLink(tabEntityKey)"
  225. :class="{
  226. '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]':
  227. collapsed,
  228. 'active:cursor-grabbing': !collapsed,
  229. }"
  230. />
  231. <UserTaskbarTabRemove
  232. :taskbar-tab="taskbarTabListByTabEntityKey[tabEntityKey]"
  233. :dirty="getTaskbarTabDirtyFlag(tabEntityKey)"
  234. :plugin="
  235. getTaskbarTabTypePlugin(
  236. taskbarTabListByTabEntityKey[tabEntityKey].type,
  237. )
  238. "
  239. />
  240. </li>
  241. </ul>
  242. </Teleport>
  243. </div>
  244. </div>
  245. </CommonLoader>
  246. </template>