feat: run macros from the command bar (#15422)

This commit is contained in:
Sivin Varghese
2026-08-13 12:47:23 +05:30
committed by GitHub
parent 416ac8660f
commit 45878923b6
4 changed files with 120 additions and 2 deletions

View File

@@ -0,0 +1,76 @@
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useMacroExecution } from 'dashboard/composables/useMacroExecution';
import { useOrderedMacros } from 'dashboard/composables/useOrderedMacros';
import { usePolicy } from 'dashboard/composables/usePolicy';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { ICON_TOY_BRICK } from 'dashboard/helper/commandbar/icons';
import {
isAConversationRoute,
isAInboxViewRoute,
} from 'dashboard/helper/routeHelpers';
export function useMacroHotKeys() {
const { t } = useI18n();
const store = useStore();
const route = useRoute();
const { orderedMacros } = useOrderedMacros();
const { execute, submitPendingAttributes, dismissPendingAttributes } =
useMacroExecution();
const { isFeatureFlagEnabled } = usePolicy();
const currentChat = useMapGetter('getSelectedChat');
const pendingAttributes = ref(null);
const isMacrosAvailable = computed(
() =>
isFeatureFlagEnabled(FEATURE_FLAGS.MACROS) &&
(isAConversationRoute(route.name) || isAInboxViewRoute(route.name))
);
watch(
isMacrosAvailable,
isActive => {
if (isActive && !orderedMacros.value.length) store.dispatch('macros/get');
},
{ immediate: true }
);
const macroHotKeys = computed(() => {
if (!isMacrosAvailable.value || !orderedMacros.value.length) {
return [];
}
const options = orderedMacros.value.map(macro => ({
id: `macro-${macro.id}`,
title: macro.name,
parent: 'execute_a_macro',
section: t('COMMAND_BAR.SECTIONS.EXECUTE_MACRO'),
icon: ICON_TOY_BRICK,
handler: () => {
pendingAttributes.value = execute(macro, currentChat.value.id);
},
}));
return [
{
id: 'execute_a_macro',
title: t('COMMAND_BAR.COMMANDS.EXECUTE_A_MACRO'),
section: t('COMMAND_BAR.SECTIONS.CONVERSATION'),
icon: ICON_TOY_BRICK,
children: options.map(option => option.id),
},
...options,
];
});
return {
macroHotKeys,
pendingAttributes,
submitPendingAttributes,
dismissPendingAttributes,
};
}

View File

@@ -161,6 +161,7 @@
"CHANGE_PRIORITY": "Change Priority",
"CHANGE_TEAM": "Change Team",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"EXECUTE_MACRO": "Execute Macro",
"ADD_LABEL": "Add label to the conversation",
"REMOVE_LABEL": "Remove label from the conversation",
"SETTINGS": "Settings",
@@ -206,6 +207,7 @@
"AI_ASSIST": "AI Assist",
"ASSIGN_PRIORITY": "Assign priority",
"ASSIGN_A_TEAM": "Assign a team",
"EXECUTE_A_MACRO": "Execute a macro",
"MUTE_CONVERSATION": "Mute conversation",
"UNMUTE_CONVERSATION": "Unmute conversation",
"REMOVE_LABEL_FROM_CONVERSATION": "Remove label from the conversation",

View File

@@ -1,6 +1,6 @@
<script setup>
import '@chatwoot/ninja-keys';
import { ref, toRef, computed, watchEffect, onMounted } from 'vue';
import { ref, toRef, computed, watch, watchEffect, onMounted } from 'vue';
import { useStore } from 'dashboard/composables/store';
import { useTrack } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
@@ -10,6 +10,8 @@ import { useInboxHotKeys } from 'dashboard/composables/commands/useInboxHotKeys'
import { useGoToCommandHotKeys } from 'dashboard/composables/commands/useGoToCommandHotKeys';
import { useBulkActionsHotKeys } from 'dashboard/composables/commands/useBulkActionsHotKeys';
import { useConversationHotKeys } from 'dashboard/composables/commands/useConversationHotKeys';
import { useMacroHotKeys } from 'dashboard/composables/commands/useMacroHotKeys';
import ConversationResolveAttributesModal from 'dashboard/components-next/ConversationWorkflow/ConversationResolveAttributesModal.vue';
import wootConstants from 'dashboard/constants/globals';
import {
GENERAL_EVENTS,
@@ -36,6 +38,7 @@ const { t, tm } = useI18n();
const { resolvedLocale } = useLocale();
const ninjakeys = ref(null);
const resolveAttributesModalRef = ref(null);
// Added selectedSnoozeType to track the selected snooze type
// So if the selected snooze type is "custom snooze" then we set selectedSnoozeType with the CMD action id
@@ -49,6 +52,21 @@ const { goToCommandHotKeys } = useGoToCommandHotKeys(
);
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
const { conversationHotKeys } = useConversationHotKeys();
const {
macroHotKeys,
pendingAttributes,
submitPendingAttributes,
dismissPendingAttributes,
} = useMacroHotKeys();
watch(pendingAttributes, pending => {
if (pending) {
resolveAttributesModalRef.value?.open(
pending.missing,
pending.customAttributes
);
}
});
const SNOOZE_PARENT_IDS = [
'snooze_conversation',
@@ -82,6 +100,7 @@ const hotKeys = computed(() => {
...goToAppearanceHotKeys.value,
...bulkActionsHotKeys.value,
...conversationHotKeys.value,
...macroHotKeys.value,
];
// When dynamic NLP snooze suggestions exist, hide all preset snooze actions to avoid duplication
if (!dynamicSnoozeActions.value.length) return allActions;
@@ -241,6 +260,11 @@ onMounted(() => {
@selected="onSelected"
@closed="onClosed"
/>
<ConversationResolveAttributesModal
ref="resolveAttributesModalRef"
@submit="submitPendingAttributes"
@close="dismissPendingAttributes"
/>
</template>
<style lang="scss">

View File

@@ -32,8 +32,11 @@ const hotKeySources = {
{ id: 'until_tomorrow', parent: 'snooze_notification' },
{ id: 'until_tomorrow', parent: 'other_menu' },
],
macroHotKeys: [{ id: 'execute_a_macro' }],
};
const pendingAttributes = ref(null);
vi.mock('dashboard/composables/commands/useAppearanceHotKeys', () => ({
useAppearanceHotKeys: () => ({
goToAppearanceHotKeys: ref(hotKeySources.goToAppearanceHotKeys),
@@ -66,6 +69,15 @@ vi.mock('dashboard/composables/commands/useConversationHotKeys', () => ({
}),
}));
vi.mock('dashboard/composables/commands/useMacroHotKeys', () => ({
useMacroHotKeys: () => ({
macroHotKeys: ref(hotKeySources.macroHotKeys),
pendingAttributes,
submitPendingAttributes: vi.fn(),
dismissPendingAttributes: vi.fn(),
}),
}));
const suggestion = {
label: 'tomorrow',
formattedDate: 'Feb 3, 9:00 AM',
@@ -100,7 +112,10 @@ describe('commandbar', () => {
let element;
const mountCommandBar = async (props = {}) => {
wrapper = mount(CommandBar, { props });
wrapper = mount(CommandBar, {
props,
global: { stubs: { ConversationResolveAttributesModal: true } },
});
await flushPromises();
element = wrapper.find('ninja-keys').element;
return wrapper;
@@ -151,6 +166,7 @@ describe('commandbar', () => {
'goto',
'bulk',
'conversation',
'execute_a_macro',
])
);
});