PersonalSettingCalendar.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { isEqual } from 'lodash-es'
  4. import { storeToRefs } from 'pinia'
  5. import { computed, reactive, ref, watch } from 'vue'
  6. import {
  7. NotificationTypes,
  8. useNotifications,
  9. } from '#shared/components/CommonNotifications/index.ts'
  10. import Form from '#shared/components/Form/Form.vue'
  11. import type {
  12. FormSchemaNode,
  13. FormValues,
  14. } from '#shared/components/Form/types.ts'
  15. import { useForm } from '#shared/components/Form/useForm.ts'
  16. import { useMultiStepForm } from '#shared/components/Form/useMultiStepForm.ts'
  17. import { defineFormSchema } from '#shared/form/defineFormSchema.ts'
  18. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  19. import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
  20. import { useSessionStore } from '#shared/stores/session.ts'
  21. import CommonInputCopyToClipboard from '#desktop/components/CommonInputCopyToClipboard/CommonInputCopyToClipboard.vue'
  22. import CommonTabManager from '#desktop/components/CommonTabManager/CommonTabManager.vue'
  23. import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
  24. import { useBreadcrumb } from '../composables/useBreadcrumb.ts'
  25. import { useUserCurrentCalendarSubscriptionUpdateMutation } from '../graphql/mutations/userCurrentCalendarSubscriptionUpdate.api.ts'
  26. import { useUserCurrentCalendarSubscriptionListQuery } from '../graphql/queries/userCurrentCalendarSubscriptionList.api.ts'
  27. const { breadcrumbItems } = useBreadcrumb(__('Calendar'))
  28. const { form, isDirty, node, formReset, formSubmit, values } = useForm()
  29. const { multiStepPlugin, allSteps, activeStep } = useMultiStepForm(node)
  30. const getFormSchemaGroupSection = (
  31. stepName: string,
  32. children: FormSchemaNode[],
  33. ) => {
  34. return {
  35. isLayout: true,
  36. element: 'section',
  37. attrs: {
  38. style: {
  39. if: `$activeStep !== "${stepName}"`,
  40. then: 'display: none;',
  41. },
  42. },
  43. children: [
  44. {
  45. type: 'group',
  46. name: stepName,
  47. isGroupOrList: true,
  48. plugins: [multiStepPlugin],
  49. children,
  50. },
  51. ],
  52. }
  53. }
  54. const escalationSection = getFormSchemaGroupSection('escalation', [
  55. {
  56. name: 'escalationOwn',
  57. type: 'toggle',
  58. label: __('My tickets'),
  59. help: __('Include your own tickets in subscription for escalated tickets.'),
  60. props: {
  61. variants: {
  62. true: 'yes',
  63. false: 'no',
  64. },
  65. },
  66. },
  67. {
  68. name: 'escalationNotAssigned',
  69. type: 'toggle',
  70. label: __('Not assigned'),
  71. help: __(
  72. 'Include unassigned tickets in subscription for escalated tickets.',
  73. ),
  74. props: {
  75. variants: {
  76. true: 'yes',
  77. false: 'no',
  78. },
  79. },
  80. },
  81. ])
  82. const newOpenSection = getFormSchemaGroupSection('newOpen', [
  83. {
  84. name: 'newOpenOwn',
  85. type: 'toggle',
  86. label: __('My tickets'),
  87. help: __(
  88. 'Include your own tickets in subscription for new & open tickets.',
  89. ),
  90. props: {
  91. variants: {
  92. true: 'yes',
  93. false: 'no',
  94. },
  95. },
  96. },
  97. {
  98. name: 'newOpenNotAssigned',
  99. type: 'toggle',
  100. label: __('Not assigned'),
  101. help: __(
  102. 'Include unassigned tickets in subscription for new & open tickets.',
  103. ),
  104. props: {
  105. variants: {
  106. true: 'yes',
  107. false: 'no',
  108. },
  109. },
  110. },
  111. ])
  112. const pendingSection = getFormSchemaGroupSection('pending', [
  113. {
  114. name: 'pendingOwn',
  115. type: 'toggle',
  116. label: __('My tickets'),
  117. help: __('Include your own tickets in subscription for pending tickets.'),
  118. props: {
  119. variants: {
  120. true: 'yes',
  121. false: 'no',
  122. },
  123. },
  124. },
  125. {
  126. name: 'pendingNotAssigned',
  127. type: 'toggle',
  128. label: __('Not assigned'),
  129. help: __('Include unassigned tickets in subscription for pending tickets.'),
  130. props: {
  131. variants: {
  132. true: 'yes',
  133. false: 'no',
  134. },
  135. },
  136. },
  137. ])
  138. const formSchema = defineFormSchema([
  139. escalationSection,
  140. newOpenSection,
  141. pendingSection,
  142. ])
  143. const schemaData = reactive({
  144. activeStep,
  145. })
  146. const calendarSubscriptionListQuery = new QueryHandler(
  147. useUserCurrentCalendarSubscriptionListQuery(),
  148. )
  149. const calendarSubscriptionListQueryResult =
  150. calendarSubscriptionListQuery.result()
  151. const { user } = storeToRefs(useSessionStore())
  152. // Refetch calendar subscription list query when the user preference has changed.
  153. watch(
  154. () => user.value?.preferences?.calendar_subscriptions,
  155. () => {
  156. calendarSubscriptionListQuery.refetch()
  157. },
  158. { deep: true },
  159. )
  160. const combinedSubscriptionURL = computed(
  161. () =>
  162. calendarSubscriptionListQueryResult.value
  163. ?.userCurrentCalendarSubscriptionList.combinedUrl ?? '',
  164. )
  165. // Alarm is a global option and therefore hoisted out of the multi-step form.
  166. // Here we keep track of its value from the query, and update it whenever is mutated from outside.
  167. const alarm = computed(() =>
  168. Boolean(
  169. calendarSubscriptionListQueryResult.value
  170. ?.userCurrentCalendarSubscriptionList.globalOptions?.alarm,
  171. ),
  172. )
  173. const alarmLocalValue = ref(alarm.value)
  174. watch(alarm, (newValue) => {
  175. alarmLocalValue.value = newValue
  176. })
  177. const directSubscriptionURL = computed(
  178. () =>
  179. calendarSubscriptionListQueryResult.value
  180. ?.userCurrentCalendarSubscriptionList[
  181. activeStep.value as 'escalation' | 'newOpen' | 'pending'
  182. ]?.url ?? '',
  183. )
  184. const formInitialValues = computed<FormValues>((oldValues) => {
  185. const values = {
  186. escalationOwn:
  187. calendarSubscriptionListQueryResult.value
  188. ?.userCurrentCalendarSubscriptionList.escalation?.options?.own,
  189. escalationNotAssigned:
  190. calendarSubscriptionListQueryResult.value
  191. ?.userCurrentCalendarSubscriptionList.escalation?.options?.notAssigned,
  192. newOpenOwn:
  193. calendarSubscriptionListQueryResult.value
  194. ?.userCurrentCalendarSubscriptionList.newOpen?.options?.own,
  195. newOpenNotAssigned:
  196. calendarSubscriptionListQueryResult.value
  197. ?.userCurrentCalendarSubscriptionList.newOpen?.options?.notAssigned,
  198. pendingOwn:
  199. calendarSubscriptionListQueryResult.value
  200. ?.userCurrentCalendarSubscriptionList.pending?.options?.own,
  201. pendingNotAssigned:
  202. calendarSubscriptionListQueryResult.value
  203. ?.userCurrentCalendarSubscriptionList.pending?.options?.notAssigned,
  204. } as unknown as FormValues
  205. if (oldValues && isEqual(values, oldValues)) return oldValues
  206. return values
  207. })
  208. watch(formInitialValues, (newValues) => {
  209. // No reset needed when the form has already the correct state.
  210. if (isEqual(values.value, newValues) && !isDirty.value) return
  211. formReset({ values: newValues })
  212. })
  213. const { notify } = useNotifications()
  214. const submitForm = async (data: FormValues) => {
  215. const input = {
  216. alarm: alarmLocalValue.value,
  217. escalation: {
  218. own: Boolean(data.escalationOwn),
  219. notAssigned: Boolean(data.escalationNotAssigned),
  220. },
  221. newOpen: {
  222. own: Boolean(data.newOpenOwn),
  223. notAssigned: Boolean(data.newOpenNotAssigned),
  224. },
  225. pending: {
  226. own: Boolean(data.pendingOwn),
  227. notAssigned: Boolean(data.pendingNotAssigned),
  228. },
  229. }
  230. const calendarSubscriptionUpdateMutation = new MutationHandler(
  231. useUserCurrentCalendarSubscriptionUpdateMutation(),
  232. {
  233. errorNotificationMessage: __(
  234. 'Updating your calendar subscription settings failed.',
  235. ),
  236. },
  237. )
  238. return calendarSubscriptionUpdateMutation.send({ input }).then(() => {
  239. notify({
  240. id: 'calendar-subscription-update-success',
  241. type: NotificationTypes.Success,
  242. message: __('You calendar subscription settings were updated.'),
  243. })
  244. })
  245. }
  246. const tabs = [
  247. {
  248. label: __('Escalated Tickets'),
  249. key: 'escalation',
  250. },
  251. {
  252. label: __('New & Open Tickets'),
  253. key: 'newOpen',
  254. },
  255. {
  256. label: __('Pending Tickets'),
  257. key: 'pending',
  258. },
  259. ]
  260. </script>
  261. <template>
  262. <LayoutContent
  263. :breadcrumb-items="breadcrumbItems"
  264. :help-text="
  265. $t(
  266. 'See your tickets from within your favorite calendar by adding the subscription URL to your calendar app.',
  267. )
  268. "
  269. width="narrow"
  270. >
  271. <div class="mb-4">
  272. <CommonInputCopyToClipboard
  273. :label="__('Combined subscription URL')"
  274. :copy-button-text="__('Copy URL')"
  275. :value="combinedSubscriptionURL"
  276. :help="__('Includes escalated, new & open and pending tickets.')"
  277. />
  278. <FormKit
  279. v-model="alarmLocalValue"
  280. type="toggle"
  281. :label="__('Add alarm to pending reminder and escalated tickets')"
  282. :variants="{ true: 'yes', false: 'no' }"
  283. @update:model-value="formSubmit"
  284. />
  285. <CommonLabel role="heading" aria-level="2" class="mb-2 mt-5" size="large">
  286. {{ $t('Subscription settings') }}
  287. </CommonLabel>
  288. <CommonTabManager v-model="activeStep" class="mb-3" :tabs="tabs" />
  289. <div
  290. :id="`tab-panel-${activeStep}`"
  291. role="tabpanel"
  292. :aria-labelledby="`tab-label-${activeStep}`"
  293. >
  294. <CommonInputCopyToClipboard
  295. :label="__('Direct subscription URL')"
  296. :copy-button-text="__('Copy URL')"
  297. :value="directSubscriptionURL"
  298. />
  299. <Form
  300. id="calendar-subscription"
  301. ref="form"
  302. :schema="formSchema"
  303. :flatten-form-groups="Object.keys(allSteps)"
  304. :initial-values="formInitialValues"
  305. :schema-data="schemaData"
  306. @changed="formSubmit"
  307. @submit="submitForm"
  308. />
  309. </div>
  310. </div>
  311. </LayoutContent>
  312. </template>