FieldSecurityInput.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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 useValue from '#shared/components/Form/composables/useValue.ts'
  7. import type {
  8. FieldSecurityProps,
  9. SecurityOption,
  10. SecurityValue,
  11. } from '#shared/components/Form/fields/FieldSecurity/types.ts'
  12. import { useFieldSecurity } from '#shared/components/Form/fields/FieldSecurity/useFieldSecurity.ts'
  13. import { useTraverseOptions } from '#shared/composables/useTraverseOptions.ts'
  14. import { translateArticleSecurity } from '#shared/entities/ticket-article/composables/translateArticleSecurity.ts'
  15. import { i18n } from '#shared/i18n.ts'
  16. const props = defineProps<FieldSecurityProps>()
  17. const contextReactive = toRef(props, 'context')
  18. const { localValue } = useValue<SecurityValue>(contextReactive)
  19. const {
  20. securityMethods,
  21. previewMethod,
  22. isCurrentSecurityOption,
  23. isSecurityOptionDisabled,
  24. changeSecurityState,
  25. } = useFieldSecurity(contextReactive, localValue)
  26. const options = computed(() => {
  27. return [
  28. {
  29. option: 'encryption',
  30. label: __('Encrypt'),
  31. icon: isCurrentSecurityOption('encryption')
  32. ? 'encryption-enabled'
  33. : 'encryption-disabled',
  34. },
  35. {
  36. option: 'sign',
  37. label: __('Sign'),
  38. icon: isCurrentSecurityOption('sign') ? 'sign-enabled' : 'sign-disabled',
  39. },
  40. ] as const
  41. })
  42. const toggleOption = (name: SecurityOption) => {
  43. if (isSecurityOptionDisabled(name)) return
  44. let currentOptions = localValue.value?.options || []
  45. if (currentOptions.includes(name))
  46. currentOptions = currentOptions.filter((option) => option !== name)
  47. else currentOptions = [...currentOptions, name]
  48. localValue.value = {
  49. method: previewMethod.value,
  50. options: currentOptions.sort(),
  51. }
  52. }
  53. const optionsContainer = ref<HTMLElement>()
  54. useTraverseOptions(optionsContainer, { direction: 'horizontal' })
  55. const tooltipMessages = computed(() => {
  56. const messages: TooltipItemDescriptor[] = []
  57. const method = previewMethod.value
  58. const { encryption, sign } = props.context.securityMessages?.[method] || {}
  59. if (encryption) {
  60. const message = i18n.t(
  61. encryption.message,
  62. ...(encryption.messagePlaceholder || []),
  63. )
  64. messages.push({
  65. type: 'text',
  66. label: `${i18n.t('Encryption:')} ${message}`,
  67. })
  68. }
  69. if (sign) {
  70. const message = i18n.t(sign.message, ...(sign.messagePlaceholder || []))
  71. messages.push({
  72. type: 'text',
  73. label: `${i18n.t('Sign:')} ${message}`,
  74. })
  75. }
  76. return messages
  77. })
  78. </script>
  79. <template>
  80. <div
  81. :id="context.id"
  82. :class="context.classes.input"
  83. class="flex h-auto flex-col gap-2"
  84. :aria-describedby="context.describedBy"
  85. v-bind="context.attrs"
  86. >
  87. <div
  88. v-if="securityMethods.length > 1"
  89. ref="typesContainer"
  90. role="listbox"
  91. class="flex flex-1 justify-between gap-2"
  92. :aria-label="$t('%s (method)', context.label)"
  93. aria-orientation="horizontal"
  94. >
  95. <button
  96. v-for="securityType of securityMethods"
  97. :key="securityType"
  98. type="button"
  99. tabindex="0"
  100. role="option"
  101. class="flex flex-1 select-none items-center justify-center rounded-md px-2 py-1"
  102. :aria-selected="previewMethod === securityType"
  103. :class="{
  104. 'bg-white font-semibold text-black': previewMethod === securityType,
  105. 'bg-gray-300': previewMethod !== securityType,
  106. }"
  107. @click="changeSecurityState(securityType)"
  108. @keydown.space.prevent="changeSecurityState(securityType)"
  109. >
  110. {{ translateArticleSecurity(securityType) }}
  111. </button>
  112. </div>
  113. <div class="flex justify-between gap-5">
  114. <CommonTooltip
  115. v-if="tooltipMessages.length"
  116. :name="`security-${context.node.name}`"
  117. :messages="tooltipMessages"
  118. :heading="__('Security Information')"
  119. >
  120. <CommonIcon name="tooltip" size="small" />
  121. </CommonTooltip>
  122. <div
  123. ref="optionsContainer"
  124. class="flex h-full items-center gap-2"
  125. role="listbox"
  126. :aria-label="$t('%s (option)', context.label)"
  127. aria-multiselectable="true"
  128. aria-orientation="horizontal"
  129. >
  130. <button
  131. v-for="{ option, label, icon } of options"
  132. :key="option"
  133. type="button"
  134. role="option"
  135. class="flex select-none items-center gap-1 rounded-md px-2 py-1 text-base"
  136. :class="{
  137. 'bg-gray-600/50 text-white/30': isSecurityOptionDisabled(option),
  138. 'cursor-pointer': !isSecurityOptionDisabled(option),
  139. 'bg-gray-300 text-white': !isCurrentSecurityOption(option),
  140. 'bg-white font-semibold text-black':
  141. isCurrentSecurityOption(option),
  142. }"
  143. :tabindex="isSecurityOptionDisabled(option) ? -1 : 0"
  144. :disabled="isSecurityOptionDisabled(option)"
  145. :aria-selected="isCurrentSecurityOption(option)"
  146. :aria-disabled="isSecurityOptionDisabled(option)"
  147. @click="toggleOption(option)"
  148. @keydown.space.prevent="toggleOption(option)"
  149. >
  150. <CommonIcon :name="icon" size="tiny" class="shrink-0" decorative />
  151. {{ $t(label) }}
  152. </button>
  153. </div>
  154. </div>
  155. </div>
  156. </template>