Browse Source

Maintenance: Desktop view - GraphQL error when opening ticket without permission on some macros.

Co-authored-by: Dominik Klein <dk@zammad.com>
Florian Liebe 2 weeks ago
parent
commit
ddf483cb4b

+ 0 - 0
app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailBottomBar/AdditionalTicketEditSubmitActions.vue


+ 45 - 42
app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/TicketDetailBottomBar/TicketDetailBottomBar.vue

@@ -15,6 +15,7 @@ import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
 import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
 import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
 import { useDialog } from '#desktop/components/CommonDialog/useDialog.ts'
+import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
 import TicketScreenBehavior from '#desktop/pages/ticket/components/TicketDetailView/TicketScreenBehavior/TicketScreenBehavior.vue'
 import { useTicketSharedDraft } from '#desktop/pages/ticket/composables/useTicketSharedDraft.ts'
 
@@ -38,6 +39,7 @@ export interface Props {
 const props = defineProps<Props>()
 
 const groupId = toRef(props, 'groupId')
+const isTicketEditable = toRef(props, 'isTicketEditable')
 
 const emit = defineEmits<{
   submit: [MouseEvent]
@@ -45,7 +47,8 @@ const emit = defineEmits<{
   'execute-macro': [MacroById]
 }>()
 
-const { macros } = useMacros(groupId)
+// For now handover ticket editable, flag, maybe later we can move the action menu in an own component.
+const { macros } = useMacros(groupId, isTicketEditable)
 
 const { notify } = useNotifications()
 
@@ -62,53 +65,52 @@ const sharedDraftConflictDialog = useDialog({
 })
 
 const actionItems = computed(() => {
-  if (!macros.value) return null
+  const saveAsDraftAction: MenuItem = {
+    label: __('Save as draft'),
+    groupLabel: groupLabels.drafts,
+    icon: 'floppy',
+    key: 'save-draft',
+    show: () => props.canUseDraft,
+    onClick: () => {
+      if (props.sharedDraftId) {
+        sharedDraftConflictDialog.open({
+          sharedDraftId: props.sharedDraftId,
+          sharedDraftParams: mapSharedDraftParams(props.ticketId, props.form),
+          form: props.form,
+        })
+
+        return
+      }
+
+      const draftCreateMutation = new MutationHandler(
+        useTicketSharedDraftZoomCreateMutation(),
+        {
+          errorNotificationMessage: __('Draft could not be saved.'),
+        },
+      )
+
+      draftCreateMutation
+        .send({ input: mapSharedDraftParams(props.ticketId, props.form) })
+        .then(() => {
+          notify({
+            id: 'shared-draft-detail-view-created',
+            type: NotificationTypes.Success,
+            message: __('Shared draft has been created successfully.'),
+          })
+        })
+    },
+  }
 
-  const macroMenu = macros.value.map((macro) => ({
+  if (!macros.value) return [saveAsDraftAction]
+
+  const macroMenu: MenuItem[] = macros.value.map((macro) => ({
     key: macro.id,
     label: macro.name,
     groupLabel: groupLabels.macros,
     onClick: () => emit('execute-macro', macro),
   }))
 
-  return [
-    {
-      label: __('Save as draft'),
-      groupLabel: groupLabels.drafts,
-      icon: 'floppy',
-      key: 'save-draft',
-      show: () => props.canUseDraft,
-      onClick: () => {
-        if (props.sharedDraftId) {
-          sharedDraftConflictDialog.open({
-            sharedDraftId: props.sharedDraftId,
-            sharedDraftParams: mapSharedDraftParams(props.ticketId, props.form),
-            form: props.form,
-          })
-
-          return
-        }
-
-        const draftCreateMutation = new MutationHandler(
-          useTicketSharedDraftZoomCreateMutation(),
-          {
-            errorNotificationMessage: __('Draft could not be saved.'),
-          },
-        )
-
-        draftCreateMutation
-          .send({ input: mapSharedDraftParams(props.ticketId, props.form) })
-          .then(() => {
-            notify({
-              id: 'shared-draft-detail-view-created',
-              type: NotificationTypes.Success,
-              message: __('Shared draft has been created successfully.'),
-            })
-          })
-      },
-    },
-    ...(groupId.value ? macroMenu : []),
-  ]
+  return [saveAsDraftAction, ...(groupId.value ? macroMenu : [])]
 })
 </script>
 
@@ -146,8 +148,9 @@ const actionItems = computed(() => {
       @click="$emit('submit', $event)"
       >{{ $t('Update') }}
     </CommonButton>
+
     <CommonActionMenu
-      v-if="isTicketAgent && actionItems"
+      v-if="isTicketAgent"
       class="flex!"
       button-size="large"
       no-single-action-mode

+ 6 - 1
app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/__tests__/TicketDetailBottomBar.spec.ts

@@ -5,6 +5,7 @@ import { ref } from 'vue'
 
 import renderComponent from '#tests/support/components/renderComponent.ts'
 import { mockPermissions } from '#tests/support/mock-permissions.ts'
+import { waitForNextTick } from '#tests/support/utils.ts'
 
 import type { TicketLiveAppUser } from '#shared/entities/ticket/types.ts'
 import { createDummyTicket } from '#shared/entities/ticket-article/__tests__/mocks/ticket.ts'
@@ -184,9 +185,13 @@ describe('TicketDetailBottomBar', () => {
         groupId: convertToGraphQLId('Group', 2),
       })
 
+      await waitForNextTick()
+
       await getMacrosUpdateSubscriptionHandler().trigger({
         macrosUpdate: {
-          macroUpdated: true,
+          macroId: convertToGraphQLId('Macro', 1),
+          groupIds: [],
+          removeMacroId: null,
         },
       })
 

+ 21 - 4
app/frontend/shared/entities/macro/composables/useMacros.ts

@@ -1,6 +1,6 @@
 // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
 
-import { computed, onBeforeUnmount, type Ref, ref } from 'vue'
+import { computed, onBeforeUnmount, type Ref, ref, watch } from 'vue'
 import { useRoute } from 'vue-router'
 
 import type { MacroById } from '#shared/entities/macro/types.ts'
@@ -19,13 +19,20 @@ export const macroScreenBehaviourMapping: Record<
   none: EnumTicketScreenBehavior.StayOnTab,
 }
 
-export const useMacros = (groupId: Ref<ID | undefined>) => {
+export const useMacros = (
+  groupId: Ref<ID | undefined>,
+  isTicketEditable: Ref<boolean>,
+) => {
+  const macroFeatureActive = computed(() =>
+    Boolean(isTicketEditable.value && groupId.value),
+  )
+
   const macroQuery = new QueryHandler(
     useMacrosQuery(
       () => ({
         groupId: groupId.value as string,
       }),
-      () => ({ enabled: !!groupId.value }),
+      () => ({ enabled: macroFeatureActive.value }),
     ),
   )
 
@@ -37,7 +44,17 @@ export const useMacros = (groupId: Ref<ID | undefined>) => {
   //   More information: https://github.com/apollographql/apollo-client/issues/10117
   const usageKey = route.meta.taskbarTabEntityKey ?? 'apply-template'
 
-  activate(usageKey, macroQuery)
+  watch(
+    macroFeatureActive,
+    (active) => {
+      if (active) {
+        activate(usageKey, macroQuery)
+      } else {
+        deactivate(usageKey)
+      }
+    },
+    { immediate: true },
+  )
 
   onBeforeUnmount(() => {
     deactivate(usageKey)

+ 3 - 1
app/frontend/shared/graphql/subscriptions/macrosUpdate.api.ts

@@ -8,7 +8,9 @@ export type ReactiveFunction<TParam> = () => TParam;
 export const MacrosUpdateDocument = gql`
     subscription macrosUpdate {
   macrosUpdate {
-    macroUpdated
+    macroId
+    groupIds
+    removeMacroId
   }
 }
     `;

+ 3 - 1
app/frontend/shared/graphql/subscriptions/macrosUpdate.graphql

@@ -1,5 +1,7 @@
 subscription macrosUpdate {
   macrosUpdate {
-    macroUpdated
+    macroId
+    groupIds
+    removeMacroId
   }
 }

+ 7 - 3
app/frontend/shared/graphql/types.ts

@@ -1402,8 +1402,12 @@ export type Macro = {
 /** Autogenerated return type of MacrosUpdate. */
 export type MacrosUpdatePayload = {
   __typename?: 'MacrosUpdatePayload';
-  /** Some macro was updated */
-  macroUpdated?: Maybe<Scalars['Boolean']['output']>;
+  /** The group IDs from the updated macro */
+  groupIds?: Maybe<Array<Scalars['ID']['output']>>;
+  /** Macro ID that was updated */
+  macroId?: Maybe<Scalars['ID']['output']>;
+  /** The macro ID that was removed */
+  removeMacroId?: Maybe<Scalars['ID']['output']>;
 };
 
 /** Mention */
@@ -6962,7 +6966,7 @@ export type CurrentUserUpdatesSubscription = { __typename?: 'Subscriptions', use
 export type MacrosUpdateSubscriptionVariables = Exact<{ [key: string]: never; }>;
 
 
-export type MacrosUpdateSubscription = { __typename?: 'Subscriptions', macrosUpdate: { __typename?: 'MacrosUpdatePayload', macroUpdated?: boolean | null } };
+export type MacrosUpdateSubscription = { __typename?: 'Subscriptions', macrosUpdate: { __typename?: 'MacrosUpdatePayload', macroId?: string | null, groupIds?: Array<string> | null, removeMacroId?: string | null } };
 
 export type PushMessagesSubscriptionVariables = Exact<{ [key: string]: never; }>;
 

+ 20 - 1
app/frontend/shared/stores/macro.ts

@@ -35,15 +35,34 @@ export const useMacroStore = defineStore('macro', () => {
   )
 
   macroSubscription.onResult((data) => {
-    if (!data.data?.macrosUpdate.macroUpdated) return
+    const macroId = data.data?.macrosUpdate.macroId
+    const groupIds = data.data?.macrosUpdate.groupIds
+    const removeMacroId = data.data?.macrosUpdate.removeMacroId
+
+    if (!macroId && !removeMacroId) return
 
     const refetchFor: Record<string, boolean> = {}
 
     queryByUsageKey.forEach((query) => {
+      const macros = query.operationResult.result.value?.macros
+
+      if (
+        !macros ||
+        (removeMacroId && !macros.find((macro) => macro.id === removeMacroId))
+      )
+        return
+
       const { groupId } = toValue(query.operationResult.variables) ?? {}
 
       // Skip refetching of duplicate queries with the same group ID.
       if (!groupId || refetchFor[groupId]) return
+      if (
+        groupIds &&
+        groupIds.length &&
+        !groupIds.includes(groupId) &&
+        !macros.find((macro) => macro.id === macroId)
+      )
+        return
 
       query.refetch()
 

+ 23 - 2
app/graphql/gql/subscriptions/macros_update.rb

@@ -4,14 +4,35 @@ module Gql::Subscriptions
   class MacrosUpdate < BaseSubscription
     description 'Updated macros'
 
-    field :macro_updated, Boolean, description: 'Some macro was updated'
+    field :macro_id, GraphQL::Types::ID, description: 'Macro ID that was updated'
+    field :group_ids, [GraphQL::Types::ID], description: 'The group IDs from the updated macro'
+    field :remove_macro_id, GraphQL::Types::ID, description: 'The macro ID that was removed'
 
     def authorized?
       true
     end
 
+    class << self
+      # Helper methods for triggering with custom payload.
+      def trigger_after_create_or_update(macro)
+        trigger({
+                  macro_id:  Gql::ZammadSchema.id_from_object(macro),
+                  group_ids: macro.group_ids.map { |id| Gql::ZammadSchema.id_from_internal_id(Group, id) },
+                  event:     :create_or_update
+                })
+      end
+
+      def trigger_after_destroy(macro)
+        trigger({ macro_id: Gql::ZammadSchema.id_from_object(macro), event: :destroy })
+      end
+    end
+
     def update
-      { macro_updated: true }
+      if object[:event] == :destroy
+        return { remove_macro_id: object[:macro_id] }
+      end
+
+      { macro_id: object[:macro_id], group_ids: object[:group_ids] }
     end
   end
 end

+ 35 - 3
app/graphql/graphql_introspection.json

@@ -8405,12 +8405,44 @@
           "description": "Autogenerated return type of MacrosUpdate.",
           "fields": [
             {
-              "name": "macroUpdated",
-              "description": "Some macro was updated",
+              "name": "groupIds",
+              "description": "The group IDs from the updated macro",
+              "args": [],
+              "type": {
+                "kind": "LIST",
+                "name": null,
+                "ofType": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "SCALAR",
+                    "name": "ID",
+                    "ofType": null
+                  }
+                }
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "macroId",
+              "description": "Macro ID that was updated",
               "args": [],
               "type": {
                 "kind": "SCALAR",
-                "name": "Boolean",
+                "name": "ID",
+                "ofType": null
+              },
+              "isDeprecated": false,
+              "deprecationReason": null
+            },
+            {
+              "name": "removeMacroId",
+              "description": "The macro ID that was removed",
+              "args": [],
+              "type": {
+                "kind": "SCALAR",
+                "name": "ID",
                 "ofType": null
               },
               "isDeprecated": false,

Some files were not shown because too many files changed in this diff