FieldSecurity.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, ref, toRef } from 'vue'
  4. import CommonTooltip from '#shared/components/CommonTooltip/CommonTooltip.vue'
  5. import type { TooltipItemDescriptor } from '#shared/components/CommonTooltip/types.ts'
  6. import { useTraverseOptions } from '#shared/composables/useTraverseOptions.ts'
  7. import { translateArticleSecurity } from '#shared/entities/ticket-article/composables/translateArticleSecurity.ts'
  8. import { i18n } from '#shared/i18n.ts'
  9. import useValue from '../../composables/useValue.ts'
  10. import { EnumSecurityStateType } from './types.ts'
  11. import type {
  12. SecurityAllowed,
  13. SecurityDefaultOptions,
  14. SecurityMessages,
  15. SecurityOption,
  16. SecurityValue,
  17. } from './types.ts'
  18. import type { FormFieldContext } from '../../types/field.ts'
  19. interface FieldSecurityProps {
  20. context: FormFieldContext<{
  21. securityAllowed?: SecurityAllowed
  22. securityDefaultOptions?: SecurityDefaultOptions
  23. securityMessages?: SecurityMessages
  24. }>
  25. }
  26. const props = defineProps<FieldSecurityProps>()
  27. const { localValue } = useValue<SecurityValue>(toRef(props, 'context'))
  28. const securityMethods = computed(() => {
  29. return Object.keys(props.context.securityAllowed || {}).sort((a) => {
  30. if (a === EnumSecurityStateType.Pgp) return -1
  31. if (a === EnumSecurityStateType.Smime) return 1
  32. return 0
  33. }) as EnumSecurityStateType[]
  34. })
  35. const previewMethod = computed(
  36. () =>
  37. localValue.value?.method ??
  38. // smime should have priority
  39. (securityMethods.value.find(
  40. (value) => value === EnumSecurityStateType.Smime,
  41. ) ||
  42. securityMethods.value[0]),
  43. )
  44. const filterOptions = (
  45. method: EnumSecurityStateType,
  46. options: SecurityOption[],
  47. ) => {
  48. return options
  49. .filter((option) =>
  50. props.context.securityAllowed?.[method]?.includes(option),
  51. )
  52. .sort()
  53. }
  54. const isCurrentValue = (option: SecurityOption) =>
  55. localValue.value?.options.includes(option) ?? false
  56. const options = computed(() => {
  57. return [
  58. {
  59. option: 'encryption',
  60. label: 'Encrypt',
  61. icon: isCurrentValue('encryption')
  62. ? 'encryption-enabled'
  63. : 'encryption-disabled',
  64. },
  65. {
  66. option: 'sign',
  67. label: 'Sign',
  68. icon: isCurrentValue('sign') ? 'sign-enabled' : 'sign-disabled',
  69. },
  70. ] as const
  71. })
  72. const isDisabled = (option: SecurityOption) =>
  73. props.context.disabled ||
  74. !props.context.securityAllowed?.[previewMethod.value]?.includes(option)
  75. const toggleOption = (name: SecurityOption) => {
  76. if (isDisabled(name)) return
  77. let currentOptions = localValue.value?.options || []
  78. if (currentOptions.includes(name))
  79. currentOptions = currentOptions.filter((option) => option !== name)
  80. else currentOptions = [...currentOptions, name]
  81. localValue.value = {
  82. method: previewMethod.value,
  83. options: currentOptions.sort(),
  84. }
  85. }
  86. const optionsContainer = ref<HTMLElement>()
  87. useTraverseOptions(optionsContainer, { direction: 'horizontal' })
  88. const tooltipMessages = computed(() => {
  89. const messages: TooltipItemDescriptor[] = []
  90. const method = previewMethod.value
  91. const { encryption, sign } = props.context.securityMessages?.[method] || {}
  92. if (encryption) {
  93. const message = i18n.t(
  94. encryption.message,
  95. ...(encryption.messagePlaceholder || []),
  96. )
  97. messages.push({
  98. type: 'text',
  99. label: `${i18n.t('Encryption:')} ${message}`,
  100. })
  101. }
  102. if (sign) {
  103. const message = i18n.t(sign.message, ...(sign.messagePlaceholder || []))
  104. messages.push({
  105. type: 'text',
  106. label: `${i18n.t('Sign:')} ${message}`,
  107. })
  108. }
  109. return messages
  110. })
  111. const defaultOptions = (method: EnumSecurityStateType) =>
  112. props.context.securityDefaultOptions?.[method] || []
  113. const changeSecurityState = (method: EnumSecurityStateType) => {
  114. // Reset the default behavior of the chosen method and remove unsupported options.
  115. const newOptions = filterOptions(method, defaultOptions(method))
  116. localValue.value = {
  117. method,
  118. options: newOptions,
  119. }
  120. }
  121. </script>
  122. <template>
  123. <div
  124. :id="`${context.node.name}-${context.formId}`"
  125. :class="context.classes.input"
  126. class="flex h-auto flex-col gap-2"
  127. >
  128. <div
  129. v-if="securityMethods.length > 1"
  130. ref="typesContainer"
  131. role="listbox"
  132. class="flex flex-1 justify-between gap-2"
  133. :aria-label="$t('%s (method)', context.label)"
  134. aria-orientation="horizontal"
  135. >
  136. <button
  137. v-for="securityType of securityMethods"
  138. :key="securityType"
  139. type="button"
  140. tabindex="0"
  141. role="option"
  142. class="flex flex-1 select-none items-center justify-center rounded-md px-2 py-1"
  143. :aria-selected="previewMethod === securityType"
  144. :class="{
  145. 'bg-white font-semibold text-black': previewMethod === securityType,
  146. 'bg-gray-300': previewMethod !== securityType,
  147. }"
  148. @click="changeSecurityState(securityType)"
  149. @keydown.space.prevent="changeSecurityState(securityType)"
  150. >
  151. {{ translateArticleSecurity(securityType) }}
  152. </button>
  153. </div>
  154. <div class="flex justify-between gap-5">
  155. <CommonTooltip
  156. v-if="tooltipMessages.length"
  157. :name="`security-${context.node.name}`"
  158. :messages="tooltipMessages"
  159. :heading="__('Security Information')"
  160. >
  161. <CommonIcon name="tooltip" size="small" />
  162. </CommonTooltip>
  163. <div
  164. ref="optionsContainer"
  165. class="flex h-full items-center gap-2"
  166. role="listbox"
  167. :aria-label="$t('%s (option)', context.label)"
  168. aria-multiselectable="true"
  169. aria-orientation="horizontal"
  170. >
  171. <button
  172. v-for="{ option, label, icon } of options"
  173. :key="option"
  174. type="button"
  175. role="option"
  176. class="flex select-none items-center gap-1 rounded-md px-2 py-1 text-base"
  177. :class="{
  178. 'bg-gray-600/50 text-white/30': isDisabled(option),
  179. 'cursor-pointer': !isDisabled(option),
  180. 'bg-gray-300 text-white': !isCurrentValue(option),
  181. 'bg-white font-semibold text-black': isCurrentValue(option),
  182. }"
  183. :tabindex="isDisabled(option) ? -1 : 0"
  184. :disabled="isDisabled(option)"
  185. :aria-selected="isCurrentValue(option)"
  186. :aria-disabled="isDisabled(option)"
  187. @click="toggleOption(option)"
  188. @keydown.space.prevent="toggleOption(option)"
  189. >
  190. <CommonIcon :name="icon" size="tiny" class="shrink-0" decorative />
  191. {{ $t(label) }}
  192. </button>
  193. </div>
  194. </div>
  195. </div>
  196. </template>