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 @@