Просмотр исходного кода

Maintenance: Desktop view - Saving notifications leads to form being invalid for a short time.

Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Tobias Schäfer <ts@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Florian Liebe 9 месяцев назад
Родитель
Сommit
190c1ba89a

+ 2 - 1
app/frontend/apps/desktop/pages/personal-setting/types/notifications.ts

@@ -1,11 +1,12 @@
 // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
 
+import type { FormValues } from '#shared/components/Form/types.ts'
 import {
   EnumNotificationSoundFile,
   type UserPersonalSettingsNotificationMatrix,
 } from '#shared/graphql/types.ts'
 
-export interface NotificationFormData {
+export interface NotificationFormData extends FormValues {
   group_ids: number[]
   file: EnumNotificationSoundFile
   enabled: boolean

+ 1 - 1
app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingCalendar.vue

@@ -235,7 +235,7 @@ watch(formInitialValues, (newValues) => {
   // No reset needed when the form has already the correct state.
   if (isEqual(values.value, newValues) && !isDirty.value) return
 
-  formReset(newValues, user.value!.personalSettings!)
+  formReset(newValues)
 })
 
 const { notify } = useNotifications()

+ 72 - 73
app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingNotifications.vue

@@ -10,10 +10,7 @@ import {
   useNotifications,
 } from '#shared/components/CommonNotifications/index.ts'
 import Form from '#shared/components/Form/Form.vue'
-import {
-  type FormSubmitData,
-  type FormValues,
-} from '#shared/components/Form/types.ts'
+import { type FormSubmitData } from '#shared/components/Form/types.ts'
 import { useForm } from '#shared/components/Form/useForm.ts'
 import { useConfirmation } from '#shared/composables/useConfirmation.ts'
 import { defineFormSchema } from '#shared/form/defineFormSchema.ts'
@@ -22,11 +19,8 @@ import {
   EnumNotificationSoundFile,
   type UserNotificationMatrixInput,
 } from '#shared/graphql/types.ts'
-import {
-  cleanupGraphQLTypename,
-  convertToGraphQLId,
-} from '#shared/graphql/utils.ts'
-import { i18n } from '#shared/i18n/index.ts'
+import { convertToGraphQLId } from '#shared/graphql/utils.ts'
+import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
 import { useSessionStore } from '#shared/stores/session.ts'
 import type { UserData } from '#shared/types/store.ts'
 
@@ -90,36 +84,29 @@ const schema = defineFormSchema([
   },
 ])
 
-const formInitialValues = computed<FormValues>((oldValues) => {
-  if (!user.value?.personalSettings) return {}
-
-  // We have to remove the __typename from the matrix to satisfy the mutation schema
-  // With the __typename, it will cause an error when the update mutation is executed
+const initialFormValues = computed<NotificationFormData>((oldValues) => {
   const { notificationConfig = {}, notificationSound = {} } =
-    cleanupGraphQLTypename(user.value.personalSettings) as Record<
-      'notificationConfig' | 'notificationSound',
-      Record<string, unknown>
-    >
+    user.value?.personalSettings || {}
 
-  const values = {
+  const values: NotificationFormData = {
     group_ids: notificationConfig?.groupIds ?? [],
-    matrix: notificationConfig?.matrix,
+    matrix: notificationConfig?.matrix || {},
 
     // Default notification sound settings are not present on the user preferences.
     file: notificationSound?.file ?? EnumNotificationSoundFile.Xylo,
     enabled: notificationSound?.enabled ?? true,
-  } as unknown as FormValues
+  }
 
   if (oldValues && isEqual(values, oldValues)) return oldValues
 
   return values
 })
 
-watch(formInitialValues, (newValues) => {
+watch(initialFormValues, (newValues) => {
   // No reset needed when the form has already the correct state.
   if (isEqual(values.value, newValues) && !isDirty.value) return
 
-  formReset(newValues, user.value!.personalSettings!)
+  formReset(newValues)
 })
 
 onChangedField('file', (fileName) => {
@@ -127,71 +114,83 @@ onChangedField('file', (fileName) => {
 })
 
 const onSubmit = async (form: FormSubmitData<NotificationFormData>) => {
-  try {
-    loading.value = true
-
-    const response =
-      await useUserCurrentNotificationPreferencesUpdateMutation().mutate({
-        matrix: form.matrix as UserNotificationMatrixInput,
-        groupIds:
-          form?.group_ids?.map((id) => convertToGraphQLId('Group', id)) || [],
-        sound: {
-          file: form.file as EnumNotificationSoundFile,
-          enabled: form.enabled,
-        },
-      })
+  loading.value = true
 
-    if (response?.data?.userCurrentNotificationPreferencesUpdate) {
-      notify({
-        id: 'notification-update-success',
-        type: NotificationTypes.Success,
-        message: i18n.t('Notification settings have been saved successfully.'),
-      })
-    }
-  } finally {
-    loading.value = false
-  }
+  const notificationUpdateMutation = new MutationHandler(
+    useUserCurrentNotificationPreferencesUpdateMutation(),
+    {
+      errorNotificationMessage: __('Notification settings could not be saved.'),
+    },
+  )
+
+  return notificationUpdateMutation
+    .send({
+      matrix: form.matrix as UserNotificationMatrixInput,
+      groupIds:
+        form?.group_ids?.map((id) => convertToGraphQLId('Group', id)) || [],
+      sound: {
+        file: form.file as EnumNotificationSoundFile,
+        enabled: form.enabled,
+      },
+    })
+    .then((response) => {
+      if (response?.userCurrentNotificationPreferencesUpdate) {
+        notify({
+          id: 'notification-update-success',
+          type: NotificationTypes.Success,
+          message: __('Notification settings have been saved successfully.'),
+        })
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
 }
 
-const resetFormToDefaults = (preferences: UserData['preferences']) => {
+const resetFormToDefaults = (
+  personalSettings: UserData['personalSettings'],
+) => {
   form.value?.resetForm({
-    matrix: preferences.notificationConfig?.matrix,
+    matrix: personalSettings?.notificationConfig?.matrix || {},
   })
 }
 
 const onResetToDefaultSettings = async () => {
-  try {
-    loading.value = true
+  const confirmed = await waitForConfirmation(
+    __('Are you sure? Your notifications settings will be reset to default.'),
+  )
 
-    const confirmed = await waitForConfirmation(
-      i18n.t(
-        'Are you sure? Your notifications settings will be reset to default.',
-      ),
-    )
+  if (!confirmed) return
 
-    if (!confirmed) return
+  loading.value = true
 
-    const response =
-      await useUserCurrentNotificationPreferencesResetMutation().mutate()
+  const notificationResetMutation = new MutationHandler(
+    useUserCurrentNotificationPreferencesResetMutation(),
+    {
+      errorNotificationMessage: __('Notification settings could not be reset.'),
+    },
+  )
 
-    const preferences =
-      response?.data?.userCurrentNotificationPreferencesReset?.user
-        ?.personalSettings
+  return notificationResetMutation
+    .send()
+    .then((response) => {
+      const personalSettings =
+        response?.userCurrentNotificationPreferencesReset?.user
+          ?.personalSettings
 
-    if (!preferences) return
+      if (!personalSettings) return
 
-    // We have to remove the __typename from the matrix to satisfy the mutation schema
-    // With the __typename, it will cause an error when the update mutation is executed
-    resetFormToDefaults(cleanupGraphQLTypename(preferences))
+      resetFormToDefaults(personalSettings)
 
-    notify({
-      id: 'notification-reset-success',
-      type: NotificationTypes.Success,
-      message: i18n.t('Notification settings have been reset to default.'),
+      notify({
+        id: 'notification-reset-success',
+        type: NotificationTypes.Success,
+        message: __('Notification settings have been reset to default.'),
+      })
+    })
+    .finally(() => {
+      loading.value = false
     })
-  } finally {
-    loading.value = false
-  }
 }
 </script>
 
@@ -203,7 +202,7 @@ const onResetToDefaultSettings = async () => {
       :schema="schema"
       :form-updater-id="EnumFormUpdaterId.FormUpdaterUpdaterUserNotifications"
       form-updater-initial-only
-      :initial-values="formInitialValues"
+      :initial-values="initialFormValues"
       @submit="onSubmit($event as FormSubmitData<NotificationFormData>)"
     >
       <template #after-fields>

+ 8 - 1
app/frontend/shared/components/Form/types.ts

@@ -24,7 +24,14 @@ export interface FormFieldAdditionalProps {
   [index: string]: unknown
 }
 
-type SimpleFormFieldValue = Primitive | Primitive[]
+type SimpleFormFieldValueBase =
+  | Primitive
+  | Primitive[]
+  | Record<string, Primitive | Primitive[]>
+
+type SimpleFormFieldValue =
+  | SimpleFormFieldValueBase
+  | Record<string, SimpleFormFieldValueBase>
 
 export type FormFieldValue =
   | SimpleFormFieldValue

+ 0 - 24
app/frontend/shared/graphql/utils.ts

@@ -35,27 +35,3 @@ export const getIdFromGraphQLId = (graphqlId: string) => {
 export const convertToGraphQLIds = (type: string, ids: (number | string)[]) => {
   return ids.map((id) => convertToGraphQLId(type, id))
 }
-
-/**
- * Recursively removes the '__typename' key from the given object and its nested objects/ array.
- *
- * @param {unknown} obj - The input object to clean up.
- * @return {unknown} The cleaned up object without the '__typename' key.
- * @info Used for graphql mutation that need payload with the appollo client added '__typename' key
- */
-export const cleanupGraphQLTypename = (obj: unknown): unknown => {
-  if (Array.isArray(obj)) {
-    return obj.map(cleanupGraphQLTypename)
-  } else if (typeof obj === 'object' && obj !== null) {
-    const newObj: Record<string, unknown> = {}
-    Object.keys(obj as Record<string, unknown>).forEach((key) => {
-      if (key !== '__typename') {
-        newObj[key] = cleanupGraphQLTypename(
-          (obj as Record<string, unknown>)[key],
-        )
-      }
-    })
-    return newObj
-  }
-  return obj
-}

+ 2 - 0
app/frontend/shared/server/apollo/link.ts

@@ -2,6 +2,7 @@
 
 import { ApolloLink, createHttpLink, from } from '@apollo/client/core'
 import { BatchHttpLink } from '@apollo/client/link/batch-http'
+import { removeTypenameFromVariables } from '@apollo/client/link/remove-typename'
 import { getMainDefinition } from '@apollo/client/utilities'
 import ActionCableLink from 'graphql-ruby-client/subscriptions/ActionCableLink'
 
@@ -96,6 +97,7 @@ const link = from([
   setAuthorizationLink,
   debugLink,
   connectedStateLink,
+  removeTypenameFromVariables(),
   splitLink,
 ])
 

+ 8 - 0
i18n/zammad.pot

@@ -9391,6 +9391,14 @@ msgstr ""
 msgid "Notification read"
 msgstr ""
 
+#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingNotifications.vue
+msgid "Notification settings could not be reset."
+msgstr ""
+
+#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingNotifications.vue
+msgid "Notification settings could not be saved."
+msgstr ""
+
 #: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingNotifications.vue
 msgid "Notification settings have been reset to default."
 msgstr ""