feat: Ability to rearrange macros in sidebar (#10879)

This commit is contained in:
Sivin Varghese
2025-02-13 17:26:05 +05:30
committed by GitHub
parent 6e48e73e73
commit 5cb8645edb
3 changed files with 183 additions and 169 deletions

View File

@@ -1,83 +1,113 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { computed, onMounted, ref } from 'vue';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useAccount } from 'dashboard/composables/useAccount';
import { useUISettings } from 'dashboard/composables/useUISettings';
import Draggable from 'vuedraggable';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import MacroItem from './MacroItem.vue';
export default {
components: {
MacroItem,
defineProps({
conversationId: {
type: [Number, String],
required: true,
},
props: {
conversationId: {
type: [Number, String],
required: true,
},
},
setup() {
const { accountScopedUrl } = useAccount();
});
return {
accountScopedUrl,
};
const store = useStore();
const { accountScopedUrl } = useAccount();
const { uiSettings, updateUISettings } = useUISettings();
const dragging = ref(false);
const macros = useMapGetter('macros/getMacros');
const uiFlags = useMapGetter('macros/getUIFlags');
const MACROS_ORDER_KEY = 'macros_display_order';
const orderedMacros = computed({
get: () => {
// Get saved order array and current macros
const savedOrder = uiSettings.value?.[MACROS_ORDER_KEY] ?? [];
const currentMacros = macros.value ?? [];
// Return unmodified macros if not present or macro is not available
if (!savedOrder.length || !currentMacros.length) {
return currentMacros;
}
// Create a Map of id -> position for faster lookups
const orderMap = new Map(savedOrder.map((id, index) => [id, index]));
return [...currentMacros].sort((a, b) => {
// Use Infinity for items not in saved order (pushes them to end)
const aPos = orderMap.get(a.id) ?? Infinity;
const bPos = orderMap.get(b.id) ?? Infinity;
return aPos - bPos;
});
},
computed: {
...mapGetters({
macros: ['macros/getMacros'],
uiFlags: 'macros/getUIFlags',
}),
},
mounted() {
this.$store.dispatch('macros/get');
set: newOrder => {
// Update settings with array of ids from new order
updateUISettings({
[MACROS_ORDER_KEY]: newOrder.map(({ id }) => id),
});
},
});
const onDragEnd = () => {
dragging.value = false;
};
onMounted(() => {
store.dispatch('macros/get');
});
</script>
<template>
<div>
<div
v-if="!uiFlags.isFetching && !macros.length"
class="macros_list--empty-state"
>
<div v-if="!uiFlags.isFetching && !macros.length" class="p-3">
<p class="flex flex-col items-center justify-center h-full">
{{ $t('MACROS.LIST.404') }}
</p>
<router-link :to="accountScopedUrl('settings/macros')">
<woot-button
variant="smooth"
icon="add"
size="tiny"
class="macros_add-button"
>
<woot-button variant="smooth" icon="add" size="tiny" class="mt-1">
{{ $t('MACROS.HEADER_BTN_TXT') }}
</woot-button>
</router-link>
</div>
<woot-loading-state
<div
v-if="uiFlags.isFetching"
:message="$t('MACROS.LOADING')"
/>
<div v-if="!uiFlags.isFetching && macros.length" class="macros-list">
<MacroItem
v-for="macro in macros"
:key="macro.id"
:macro="macro"
:conversation-id="conversationId"
/>
class="flex items-center gap-2 justify-center p-6 text-n-slate-12"
>
<span class="text-sm">{{ $t('MACROS.LOADING') }}</span>
<Spinner class="size-5" />
</div>
<Draggable
v-if="!uiFlags.isFetching && macros.length"
v-model="orderedMacros"
class="p-1"
animation="200"
ghost-class="ghost"
handle=".drag-handle"
item-key="id"
@start="dragging = true"
@end="onDragEnd"
>
<template #item="{ element }">
<MacroItem
:key="element.id"
:macro="element"
:conversation-id="conversationId"
class="drag-handle cursor-grab"
/>
</template>
</Draggable>
</div>
</template>
<style scoped lang="scss">
.macros-list {
padding: var(--space-smaller);
}
.macros_list--empty-state {
padding: var(--space-slab);
p {
margin: 0;
}
}
.macros_add-button {
margin: var(--space-small) auto 0;
.ghost {
@apply opacity-50 bg-n-slate-3 dark:bg-n-slate-9;
}
</style>

View File

@@ -1,75 +1,78 @@
<script>
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import MacroPreview from './MacroPreview.vue';
import { useStore } from 'dashboard/composables/store';
import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import { useTrack } from 'dashboard/composables';
export default {
components: {
MacroPreview,
import NextButton from 'dashboard/components-next/button/Button.vue';
import MacroPreview from './MacroPreview.vue';
const props = defineProps({
macro: {
type: Object,
required: true,
},
props: {
macro: {
type: Object,
required: true,
},
conversationId: {
type: [Number, String],
required: true,
},
},
data() {
return {
isExecuting: false,
showPreview: false,
};
},
methods: {
async executeMacro(macro) {
try {
this.isExecuting = true;
await this.$store.dispatch('macros/execute', {
macroId: macro.id,
conversationIds: [this.conversationId],
});
useTrack(CONVERSATION_EVENTS.EXECUTED_A_MACRO);
useAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
} catch (error) {
useAlert(this.$t('MACROS.ERROR'));
} finally {
this.isExecuting = false;
}
},
toggleMacroPreview() {
this.showPreview = !this.showPreview;
},
closeMacroPreview() {
this.showPreview = false;
},
conversationId: {
type: [Number, String],
required: true,
},
});
const store = useStore();
const { t } = useI18n();
const isExecuting = ref(false);
const showPreview = ref(false);
const executeMacro = async macro => {
try {
isExecuting.value = true;
await store.dispatch('macros/execute', {
macroId: macro.id,
conversationIds: [props.conversationId],
});
useTrack(CONVERSATION_EVENTS.EXECUTED_A_MACRO);
useAlert(t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
} catch (error) {
useAlert(t('MACROS.ERROR'));
} finally {
isExecuting.value = false;
}
};
const toggleMacroPreview = () => {
showPreview.value = !showPreview.value;
};
const closeMacroPreview = () => {
showPreview.value = false;
};
</script>
<template>
<div class="macro button secondary clear">
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
macro.name
}}</span>
<div class="flex items-center gap-1 macros-actions">
<woot-button
<div
class="relative flex items-center justify-between leading-4 rounded-md button secondary clear"
>
<span class="overflow-hidden whitespace-nowrap text-ellipsis">
{{ macro.name }}
</span>
<div class="flex items-center gap-1 justify-end">
<NextButton
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
size="tiny"
variant="smooth"
color-scheme="secondary"
icon="info"
@click="toggleMacroPreview(macro)"
icon="i-lucide-info"
slate
faded
xs
@click="toggleMacroPreview"
/>
<woot-button
<NextButton
v-tooltip.left-start="$t('MACROS.EXECUTE.BUTTON_TOOLTIP')"
size="tiny"
variant="smooth"
color-scheme="secondary"
icon="play-circle"
icon="i-lucide-play"
slate
faded
xs
:is-loading="isExecuting"
@click="executeMacro(macro)"
/>
@@ -83,13 +86,3 @@ export default {
</transition>
</div>
</template>
<style scoped lang="scss">
.macro {
@apply relative flex items-center justify-between leading-4 rounded-md;
.macros-actions {
@apply flex items-center justify-end;
}
}
</style>

View File

@@ -1,57 +1,48 @@
<script>
<script setup>
import { computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store.js';
import {
resolveActionName,
resolveTeamIds,
resolveLabels,
resolveAgents,
} from 'dashboard/routes/dashboard/settings/macros/macroHelper';
import { mapGetters } from 'vuex';
export default {
props: {
macro: {
type: Object,
required: true,
},
},
computed: {
resolvedMacro() {
return this.macro.actions.map(action => {
return {
actionName: resolveActionName(action.action_name),
actionValue: this.getActionValue(
action.action_name,
action.action_params
),
};
});
},
...mapGetters({
labels: 'labels/getLabels',
teams: 'teams/getTeams',
agents: 'agents/getAgents',
}),
},
methods: {
getActionValue(key, params) {
const actionsMap = {
assign_team: resolveTeamIds(this.teams, params),
add_label: resolveLabels(this.labels, params),
remove_label: resolveLabels(this.labels, params),
assign_agent: resolveAgents(this.agents, params),
mute_conversation: null,
snooze_conversation: null,
resolve_conversation: null,
remove_assigned_team: null,
send_webhook_event: params[0],
send_message: params[0],
send_email_transcript: params[0],
add_private_note: params[0],
};
return actionsMap[key] || '';
},
const props = defineProps({
macro: {
type: Object,
required: true,
},
});
const labels = useMapGetter('labels/getLabels');
const teams = useMapGetter('teams/getTeams');
const agents = useMapGetter('agents/getAgents');
const getActionValue = (key, params) => {
const actionsMap = {
assign_team: resolveTeamIds(teams.value, params),
add_label: resolveLabels(labels.value, params),
remove_label: resolveLabels(labels.value, params),
assign_agent: resolveAgents(agents.value, params),
mute_conversation: null,
snooze_conversation: null,
resolve_conversation: null,
remove_assigned_team: null,
send_webhook_event: params[0],
send_message: params[0],
send_email_transcript: params[0],
add_private_note: params[0],
};
return actionsMap[key] || '';
};
const resolvedMacro = computed(() => {
return props.macro.actions.map(action => ({
actionName: resolveActionName(action.action_name),
actionValue: getActionValue(action.action_name, action.action_params),
}));
});
</script>
<template>