FieldPermissionsInput.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { cloneDeep } from 'lodash-es'
  4. import { computed, ref, toRef } from 'vue'
  5. import useValue from '#shared/components/Form/composables/useValue.ts'
  6. import { useDelegateFocus } from '#shared/composables/useDelegateFocus.ts'
  7. import { i18n } from '#shared/i18n.ts'
  8. import { useTransitionCollapse } from '#desktop/composables/useTransitionCollapse.ts'
  9. import type { PermissionsChildOption, PermissionsProps } from './types.ts'
  10. const props = defineProps<{
  11. context: PermissionsProps
  12. }>()
  13. const contextReactive = toRef(props, 'context')
  14. const { localValue } = useValue<string[] | undefined>(contextReactive)
  15. const valueLookup = computed<Record<string, boolean>>(() => {
  16. const values: string[] = localValue.value || []
  17. return values.reduce((value: Record<string, boolean>, key) => {
  18. value[key] = true
  19. return value
  20. }, {})
  21. })
  22. const parentChildLookup = ref(
  23. props.context.options.reduce(
  24. (lookup: Record<string, PermissionsChildOption[]>, option) => {
  25. lookup[option.value] = option.children
  26. return lookup
  27. },
  28. {},
  29. ),
  30. )
  31. const initializeCollapseState = (key: string) =>
  32. !!localValue.value?.some((value: string) =>
  33. parentChildLookup.value[key]?.some((option) => option.value === value),
  34. )
  35. const collapseLookup = ref(
  36. props.context.options.reduce((lookup: Record<string, boolean>, option) => {
  37. lookup[option.value] = initializeCollapseState(option.value)
  38. return lookup
  39. }, {}),
  40. )
  41. const updateValue = (key: string, state: boolean | undefined) => {
  42. const values: string[] = cloneDeep(localValue.value) || []
  43. if (state === true && !values.includes(key)) {
  44. values.push(key)
  45. localValue.value = values
  46. collapseLookup.value[key] = false
  47. } else if (state === false) {
  48. localValue.value = values.filter((value) => value !== key)
  49. collapseLookup.value[key] = initializeCollapseState(key)
  50. }
  51. }
  52. const toggleCollapse = (value: string) => {
  53. collapseLookup.value[value] = !collapseLookup.value[value]
  54. }
  55. const { delegateFocus } = useDelegateFocus(
  56. props.context.id,
  57. `permissions_toggle_${props.context.id}_${props.context?.options && props.context?.options[0]?.value}`,
  58. )
  59. const { collapseDuration, collapseEnter, collapseAfterEnter, collapseLeave } =
  60. useTransitionCollapse()
  61. </script>
  62. <template>
  63. <output
  64. :id="context.id"
  65. class="block rounded-lg bg-blue-200 focus:outline focus:outline-1 focus:outline-offset-1 focus:outline-blue-800 hover:focus:outline-blue-800 dark:bg-gray-700"
  66. role="tree"
  67. :class="context.classes.input"
  68. :name="context.node.name"
  69. :aria-disabled="context.disabled"
  70. :aria-describedby="context.describedBy"
  71. :tabindex="context.disabled ? '-1' : '0'"
  72. v-bind="context.attrs"
  73. @focus="delegateFocus"
  74. >
  75. <div
  76. v-for="(option, index) in context.options"
  77. :key="`option-${option.value}`"
  78. class="flex flex-col"
  79. >
  80. <div
  81. class="flex items-center gap-2.5 px-3 py-2.5"
  82. role="treeitem"
  83. :aria-selected="valueLookup[option.value]"
  84. >
  85. <FormKit
  86. :id="`permissions_toggle_${context.id}_${option.value}`"
  87. :model-value="valueLookup[option.value]"
  88. type="toggle"
  89. :name="`permissions_toggle_${context.id}_${option.value}`"
  90. :ignore="true"
  91. outer-class="grow"
  92. wrapper-class="justify-end gap-2.5 formkit-disabled:opacity-100"
  93. inner-class="formkit-disabled:opacity-50"
  94. :variants="{ true: 'True', false: 'False' }"
  95. :disabled="context.disabled || option.disabled"
  96. size="small"
  97. :label="option.label"
  98. :sections-schema="{
  99. label: {
  100. attrs: {
  101. class: 'flex flex-col cursor-pointer',
  102. for: `permissions_toggle_${context.id}_${option.value}`,
  103. tabindex: '-1',
  104. },
  105. children: [
  106. {
  107. $cmp: 'CommonLabel',
  108. props: {
  109. class: 'text-black dark:text-white',
  110. },
  111. children: [
  112. {
  113. $el: 'div',
  114. attrs: {
  115. class: 'shrink-0',
  116. },
  117. children: i18n.t(option.label),
  118. },
  119. {
  120. $cmp: 'CommonBadge',
  121. props: {
  122. class: 'inline truncate',
  123. variant: 'neutral',
  124. },
  125. children: option.value,
  126. },
  127. ],
  128. },
  129. {
  130. $cmp: 'CommonLabel',
  131. props: {
  132. class: 'text-stone-200 dark:text-neutral-500',
  133. },
  134. children: i18n.t(option.description),
  135. },
  136. ],
  137. },
  138. }"
  139. @update:model-value="updateValue(option.value, $event)"
  140. @blur="index === 0 ? context.handlers.blur : undefined"
  141. />
  142. <CommonIcon
  143. v-if="option.children && !valueLookup[option.value]"
  144. class="shrink-0 fill-stone-200 hover:fill-black focus:outline-none focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
  145. :aria-label="i18n.t('Toggle Group')"
  146. :name="collapseLookup[option.value] ? 'chevron-up' : 'chevron-down'"
  147. size="xs"
  148. role="button"
  149. tabindex="0"
  150. @click.stop="toggleCollapse(option.value)"
  151. @keypress.enter.prevent.stop="toggleCollapse(option.value)"
  152. @keypress.space.prevent.stop="toggleCollapse(option.value)"
  153. />
  154. </div>
  155. <Transition
  156. name="collapse"
  157. :duration="collapseDuration"
  158. @enter="collapseEnter"
  159. @after-enter="collapseAfterEnter"
  160. @leave="collapseLeave"
  161. >
  162. <div
  163. v-if="option.children"
  164. v-show="collapseLookup[option.value]"
  165. class="ms-10 flex flex-col"
  166. role="group"
  167. >
  168. <div
  169. v-for="childOption in option.children"
  170. :key="`child-option-${childOption.value}`"
  171. class="flex gap-2.5 px-3 py-2.5"
  172. role="treeitem"
  173. :aria-selected="valueLookup[childOption.value]"
  174. >
  175. <FormKit
  176. :id="`permissions_child_toggle_${context.id}_${childOption.value}`"
  177. :model-value="valueLookup[childOption.value]"
  178. type="toggle"
  179. :name="`permissions_child_toggle_${context.id}_${childOption.value}`"
  180. :ignore="true"
  181. wrapper-class="gap-2.5"
  182. :variants="{ true: 'True', false: 'False' }"
  183. :disabled="context.disabled"
  184. size="small"
  185. :label="childOption.label"
  186. :sections-schema="{
  187. label: {
  188. attrs: {
  189. class: 'flex flex-col cursor-pointer',
  190. for: `permissions_child_toggle_${context.id}_${childOption.value}`,
  191. tabindex: '-1',
  192. },
  193. children: [
  194. {
  195. $cmp: 'CommonLabel',
  196. props: {
  197. class: 'text-black dark:text-white',
  198. },
  199. children: [
  200. {
  201. $el: 'div',
  202. attrs: {
  203. class: 'shrink-0',
  204. },
  205. children: i18n.t(childOption.label),
  206. },
  207. {
  208. $cmp: 'CommonBadge',
  209. props: {
  210. class: 'inline truncate',
  211. variant: 'neutral',
  212. },
  213. children: childOption.value,
  214. },
  215. ],
  216. },
  217. {
  218. $cmp: 'CommonLabel',
  219. props: {
  220. class: 'text-stone-200 dark:text-neutral-500',
  221. },
  222. children: i18n.t(childOption.description),
  223. },
  224. ],
  225. },
  226. }"
  227. @update:model-value="updateValue(childOption.value, $event)"
  228. />
  229. </div>
  230. </div>
  231. </Transition>
  232. </div>
  233. </output>
  234. </template>