From 9177fffa71adc2b5afba913614f3bb6c053561af Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:59:05 +0530 Subject: [PATCH] fix: enforce required conversation attributes on resolve for macros (#15232) --- .../ConversationResolveAttributesModal.vue | 11 +- .../useConversationRequiredAttributes.spec.js | 22 ++-- .../useConversationRequiredAttributes.js | 5 +- .../dashboard/i18n/locale/en/macros.json | 3 +- .../dashboard/conversation/Macros/List.vue | 104 +++++++++++++++++- .../conversation/Macros/MacroItem.vue | 35 +----- .../store/modules/conversations/actions.js | 2 +- app/services/macros/execution_service.rb | 2 + .../enterprise/macros/execution_service.rb | 32 ++++++ .../services/macros/execution_service_spec.rb | 96 ++++++++++++++++ 10 files changed, 264 insertions(+), 48 deletions(-) create mode 100644 enterprise/app/services/enterprise/macros/execution_service.rb create mode 100644 spec/enterprise/services/macros/execution_service_spec.rb diff --git a/app/javascript/dashboard/components-next/ConversationWorkflow/ConversationResolveAttributesModal.vue b/app/javascript/dashboard/components-next/ConversationWorkflow/ConversationResolveAttributesModal.vue index 12e13f11e..7603a6013 100644 --- a/app/javascript/dashboard/components-next/ConversationWorkflow/ConversationResolveAttributesModal.vue +++ b/app/javascript/dashboard/components-next/ConversationWorkflow/ConversationResolveAttributesModal.vue @@ -11,7 +11,7 @@ import Input from 'dashboard/components-next/input/Input.vue'; import ChoiceToggle from 'dashboard/components-next/input/ChoiceToggle.vue'; import { ATTRIBUTE_TYPES } from './constants'; -const emit = defineEmits(['submit']); +const emit = defineEmits(['submit', 'close']); const { t } = useI18n(); @@ -105,8 +105,6 @@ const comboBoxOptions = computed(() => { const close = () => { dialogRef.value?.close(); - conversationContext.value = null; - v$.value.$reset(); }; const open = (attributes = [], initialValues = {}, context = null) => { @@ -135,6 +133,12 @@ const open = (attributes = [], initialValues = {}, context = null) => { dialogRef.value?.open(); }; +const handleClose = () => { + conversationContext.value = null; + v$.value.$reset(); + emit('close'); +}; + const handleConfirm = async () => { v$.value.$touch(); if (v$.value.$invalid) { @@ -167,6 +171,7 @@ defineExpose({ open, close }); " :disable-confirm-button="!isFormComplete" @confirm="handleConfirm" + @close="handleClose" >
{ expect(result.missing[0].value).toBe('category'); }); - it('should consider undefined checkbox values as present when key exists', () => { + it('should detect missing checkbox when the key exists without a value', () => { const { checkMissingAttributes } = useConversationRequiredAttributes(); - const customAttributes = { - priority: 'High', - category: 'Bug Report', - is_urgent: undefined, // key exists but value is undefined - still considered "filled" for checkbox - }; + // The backend reads nil as missing, so the prompt has to open for it too. + [null, undefined].forEach(unset => { + const result = checkMissingAttributes({ + priority: 'High', + category: 'Bug Report', + is_urgent: unset, + }); - const result = checkMissingAttributes(customAttributes); - - expect(result.hasMissing).toBe(false); - expect(result.missing).toEqual([]); + expect(result.hasMissing).toBe(true); + expect(result.missing).toHaveLength(1); + expect(result.missing[0].value).toBe('is_urgent'); + }); }); it('should return no missing when no attributes are required', () => { diff --git a/app/javascript/dashboard/composables/useConversationRequiredAttributes.js b/app/javascript/dashboard/composables/useConversationRequiredAttributes.js index 7b6ab29b1..51c69ea3b 100644 --- a/app/javascript/dashboard/composables/useConversationRequiredAttributes.js +++ b/app/javascript/dashboard/composables/useConversationRequiredAttributes.js @@ -72,9 +72,10 @@ export function useConversationRequiredAttributes() { const missing = requiredAttributes.value.filter(attribute => { const value = conversationCustomAttributes[attribute.value]; - // For checkbox/boolean attributes, only check if the key exists + // A checkbox is filled once true or false is picked. An explicit null is + // an unanswered prompt, which is how the modal and the backend read it. if (attribute.type === ATTRIBUTE_TYPES.CHECKBOX) { - return !(attribute.value in conversationCustomAttributes); + return value == null; } // For other attribute types, only consider null, undefined, empty string, or whitespace-only as missing diff --git a/app/javascript/dashboard/i18n/locale/en/macros.json b/app/javascript/dashboard/i18n/locale/en/macros.json index e51975921..342a7ae51 100644 --- a/app/javascript/dashboard/i18n/locale/en/macros.json +++ b/app/javascript/dashboard/i18n/locale/en/macros.json @@ -82,7 +82,8 @@ "EXECUTE": { "BUTTON_TOOLTIP": "Execute", "PREVIEW": "Preview Macro", - "EXECUTED_SUCCESSFULLY": "Macro executed successfully" + "EXECUTED_SUCCESSFULLY": "Macro executed successfully", + "EXECUTED_WITHOUT_RESOLVING": "Macro executed. The conversation was not resolved because required attributes are missing." }, "ERRORS": { "ATTRIBUTE_KEY_REQUIRED": "Attribute key is required", diff --git a/app/javascript/dashboard/routes/dashboard/conversation/Macros/List.vue b/app/javascript/dashboard/routes/dashboard/conversation/Macros/List.vue index 7903ae751..07fdaa4c8 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/Macros/List.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/Macros/List.vue @@ -1,15 +1,20 @@