FieldGroupPermissionsInput.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { cloneDeep, isEqual } from 'lodash-es'
  4. import { computed, reactive, toRef, watch } from 'vue'
  5. import type { SelectValue } from '#shared/components/CommonSelect/types.ts'
  6. import useValue from '#shared/components/Form/composables/useValue.ts'
  7. import type { TreeSelectOption } from '#shared/components/Form/fields/FieldTreeSelect/types.ts'
  8. import { useDelegateFocus } from '#shared/composables/useDelegateFocus.ts'
  9. import getUuid from '#shared/utils/getUuid.ts'
  10. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  11. import useFlatSelectOptions from '../FieldTreeSelect/useFlatSelectOptions.ts'
  12. import {
  13. GroupAccess,
  14. type GroupPermissionReactive,
  15. type GroupPermissionsContext,
  16. } from './types.ts'
  17. interface Props {
  18. context: GroupPermissionsContext
  19. }
  20. const props = defineProps<Props>()
  21. const contextReactive = toRef(props, 'context')
  22. const { localValue } = useValue(contextReactive)
  23. const { flatOptions } = useFlatSelectOptions(toRef(props.context, 'options'))
  24. const groupPermissions = reactive<GroupPermissionReactive[]>([])
  25. const groupOptions = reactive<TreeSelectOption[][]>([])
  26. const groupAccesses = [
  27. {
  28. access: GroupAccess.Read,
  29. label: __('Read'),
  30. },
  31. {
  32. access: GroupAccess.Create,
  33. label: __('Create'),
  34. },
  35. {
  36. access: GroupAccess.Change,
  37. label: __('Change'),
  38. },
  39. {
  40. access: GroupAccess.Overview,
  41. label: __('Overview'),
  42. },
  43. {
  44. access: GroupAccess.Full,
  45. label: __('Full'),
  46. },
  47. ]
  48. const getTakenGroups = (index: number): SelectValue[] =>
  49. groupPermissions.reduce((takenGroups, groupPermission, currentIndex) => {
  50. if (currentIndex !== index && groupPermission.groups)
  51. takenGroups.push(...(groupPermission.groups as unknown as SelectValue[]))
  52. return takenGroups
  53. }, [] as SelectValue[])
  54. const filterTreeSelectOptions = (options: TreeSelectOption[], index: number) =>
  55. options.filter((group) => {
  56. if (group.children) {
  57. const children = filterTreeSelectOptions(group.children, index)
  58. if (children.length) {
  59. group.children = children
  60. // Set the parent option to disabled in case it's taken, but there are child options available.
  61. group.disabled = getTakenGroups(index).includes(group.value)
  62. return true
  63. }
  64. }
  65. // Remove empty children options.
  66. delete group.children
  67. return !getTakenGroups(index).includes(group.value)
  68. })
  69. const filterGroupOptions = (index: number) =>
  70. filterTreeSelectOptions(cloneDeep(contextReactive.value.options || []), index)
  71. const getNewGroupPermission = () => ({
  72. key: getUuid(),
  73. groups: [] as unknown as SelectValue,
  74. groupAccess: groupAccesses.reduce(
  75. (groupAccess, { access }) => {
  76. groupAccess[access] = false
  77. return groupAccess
  78. },
  79. {} as Record<GroupAccess, boolean>,
  80. ),
  81. })
  82. const addGroupPermission = (index: number) => {
  83. groupOptions[index] = filterGroupOptions(index)
  84. groupPermissions.splice(index, 0, getNewGroupPermission())
  85. }
  86. const removeGroupPermission = (index: number) => {
  87. groupPermissions.splice(index, 1)
  88. groupOptions.splice(index, 1)
  89. }
  90. watch(
  91. groupPermissions,
  92. (newValue) => {
  93. // Set external value to internal one, but only if they differ (loop protection).
  94. if (isEqual(newValue, localValue.value)) return
  95. newValue.forEach((_groupPermission, index) => {
  96. groupOptions[index] = filterGroupOptions(index)
  97. })
  98. localValue.value = cloneDeep(newValue)
  99. },
  100. {
  101. deep: true,
  102. },
  103. )
  104. watch(
  105. localValue,
  106. (newValue) => {
  107. if (!newValue || !newValue.length) {
  108. groupOptions.splice(0, groupOptions.length, filterGroupOptions(0))
  109. groupPermissions.splice(
  110. 0,
  111. groupPermissions.length,
  112. getNewGroupPermission(),
  113. )
  114. return
  115. }
  116. // Set internal value to external one, but only if they differ (loop protection).
  117. if (isEqual(newValue, groupPermissions)) return
  118. const newValues = cloneDeep(newValue || []) as GroupPermissionReactive[]
  119. newValues.forEach((groupPermission, index) => {
  120. groupPermission.key = getUuid()
  121. groupOptions[index] = filterGroupOptions(index)
  122. })
  123. groupPermissions.splice(0, groupPermissions.length, ...newValues)
  124. },
  125. {
  126. immediate: true,
  127. },
  128. )
  129. const hasLastGroupPermission = computed(() => groupPermissions.length === 1)
  130. const hasNoMoreGroups = computed(
  131. () =>
  132. !flatOptions.value.length ||
  133. groupPermissions.reduce((emptyGroups, groupPermission) => {
  134. if (!((groupPermission.groups as unknown as SelectValue[]) || []).length)
  135. emptyGroups += 1
  136. return emptyGroups
  137. }, 0) > 0 ||
  138. groupPermissions.reduce(
  139. (selectedGroupCount, groupPermission) =>
  140. selectedGroupCount +
  141. ((groupPermission.groups as unknown as SelectValue[]) || []).length,
  142. 0,
  143. ) === flatOptions.value.length,
  144. )
  145. const { delegateFocus } = useDelegateFocus(
  146. contextReactive.value.id,
  147. `${contextReactive.value.id}_first_element`,
  148. )
  149. const ensureGranularOrFullAccess = (
  150. groupAccess: Record<GroupAccess, boolean>,
  151. access: GroupAccess,
  152. value: boolean,
  153. ) => {
  154. if (value === false) return
  155. if (access === GroupAccess.Full && value === true) {
  156. Object.entries(groupAccess).forEach(([key, state]) => {
  157. if (key !== GroupAccess.Full && state === true) {
  158. groupAccess[key as GroupAccess] = false
  159. }
  160. })
  161. } else if (
  162. access !== GroupAccess.Full &&
  163. groupAccess[GroupAccess.Full] === true
  164. )
  165. groupAccess[GroupAccess.Full] = false
  166. }
  167. </script>
  168. <template>
  169. <output
  170. :id="context.id"
  171. class="flex w-full flex-col space-y-2 rounded-lg p-2 focus:outline focus:outline-1 focus:outline-offset-1 focus:outline-blue-800 hover:focus:outline-blue-800"
  172. :class="context.classes.input"
  173. :name="context.node.name"
  174. role="list"
  175. :tabindex="context.disabled ? '-1' : '0'"
  176. :aria-disabled="context.disabled"
  177. :aria-describedby="context.describedBy"
  178. v-bind="context.attrs"
  179. @focus="delegateFocus"
  180. >
  181. <div
  182. v-for="(groupPermission, index) in groupPermissions"
  183. :key="groupPermission.key"
  184. class="flex w-full items-center gap-3"
  185. role="listitem"
  186. >
  187. <FormKit
  188. :id="index === 0 ? `${context.id}_first_element` : undefined"
  189. v-model="groupPermission.groups"
  190. type="treeselect"
  191. outer-class="grow"
  192. :ignore="true"
  193. :options="groupOptions[index]"
  194. :clearable="true"
  195. :multiple="true"
  196. :disabled="context.disabled"
  197. :alternative-background="true"
  198. :no-options-label-translation="true"
  199. @blur="index === 0 ? context.handlers.blur : undefined"
  200. />
  201. <FormKit
  202. v-for="groupAccess in groupAccesses"
  203. :key="groupAccess.access"
  204. v-model="groupPermission.groupAccess[groupAccess.access]"
  205. type="checkbox"
  206. wrapper-class="shrink-0 flex-col-reverse"
  207. :ignore="true"
  208. :disabled="context.disabled"
  209. :alternative-border="true"
  210. @input="
  211. ensureGranularOrFullAccess(
  212. groupPermission.groupAccess,
  213. groupAccess.access,
  214. $event!,
  215. )
  216. "
  217. >
  218. <template #label>
  219. <CommonLabel
  220. class="uppercase text-gray-300 dark:text-neutral-400"
  221. size="small"
  222. >
  223. {{ $t(groupAccess.label) }}
  224. </CommonLabel>
  225. </template>
  226. </FormKit>
  227. <CommonButton
  228. class="shrink-0 text-gray-300 dark:text-neutral-400"
  229. icon="dash-circle"
  230. size="medium"
  231. :aria-label="$t('Remove')"
  232. :disabled="hasLastGroupPermission"
  233. :tabindex="hasLastGroupPermission ? '-1' : '0'"
  234. @click="removeGroupPermission(index)"
  235. />
  236. <CommonButton
  237. class="me-2.5 shrink-0 text-gray-300 dark:text-neutral-400"
  238. icon="plus-circle"
  239. size="medium"
  240. :aria-label="$t('Add')"
  241. :disabled="hasNoMoreGroups"
  242. :tabindex="hasNoMoreGroups ? '-1' : '0'"
  243. @click="addGroupPermission(index + 1)"
  244. />
  245. </div>
  246. </output>
  247. </template>