feat: Ability to rearrange macros in sidebar (#10879)
This commit is contained in:
@@ -1,83 +1,113 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
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';
|
import MacroItem from './MacroItem.vue';
|
||||||
|
|
||||||
export default {
|
defineProps({
|
||||||
components: {
|
|
||||||
MacroItem,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
setup() {
|
|
||||||
const { accountScopedUrl } = useAccount();
|
|
||||||
|
|
||||||
return {
|
const store = useStore();
|
||||||
accountScopedUrl,
|
const { accountScopedUrl } = useAccount();
|
||||||
};
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
},
|
|
||||||
computed: {
|
const dragging = ref(false);
|
||||||
...mapGetters({
|
|
||||||
macros: ['macros/getMacros'],
|
const macros = useMapGetter('macros/getMacros');
|
||||||
uiFlags: 'macros/getUIFlags',
|
const uiFlags = useMapGetter('macros/getUIFlags');
|
||||||
}),
|
|
||||||
},
|
const MACROS_ORDER_KEY = 'macros_display_order';
|
||||||
mounted() {
|
|
||||||
this.$store.dispatch('macros/get');
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div v-if="!uiFlags.isFetching && !macros.length" class="p-3">
|
||||||
v-if="!uiFlags.isFetching && !macros.length"
|
|
||||||
class="macros_list--empty-state"
|
|
||||||
>
|
|
||||||
<p class="flex flex-col items-center justify-center h-full">
|
<p class="flex flex-col items-center justify-center h-full">
|
||||||
{{ $t('MACROS.LIST.404') }}
|
{{ $t('MACROS.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
<router-link :to="accountScopedUrl('settings/macros')">
|
<router-link :to="accountScopedUrl('settings/macros')">
|
||||||
<woot-button
|
<woot-button variant="smooth" icon="add" size="tiny" class="mt-1">
|
||||||
variant="smooth"
|
|
||||||
icon="add"
|
|
||||||
size="tiny"
|
|
||||||
class="macros_add-button"
|
|
||||||
>
|
|
||||||
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<woot-loading-state
|
<div
|
||||||
v-if="uiFlags.isFetching"
|
v-if="uiFlags.isFetching"
|
||||||
:message="$t('MACROS.LOADING')"
|
class="flex items-center gap-2 justify-center p-6 text-n-slate-12"
|
||||||
/>
|
>
|
||||||
<div v-if="!uiFlags.isFetching && macros.length" class="macros-list">
|
<span class="text-sm">{{ $t('MACROS.LOADING') }}</span>
|
||||||
<MacroItem
|
<Spinner class="size-5" />
|
||||||
v-for="macro in macros"
|
|
||||||
:key="macro.id"
|
|
||||||
:macro="macro"
|
|
||||||
:conversation-id="conversationId"
|
|
||||||
/>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.macros-list {
|
.ghost {
|
||||||
padding: var(--space-smaller);
|
@apply opacity-50 bg-n-slate-3 dark:bg-n-slate-9;
|
||||||
}
|
|
||||||
.macros_list--empty-state {
|
|
||||||
padding: var(--space-slab);
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.macros_add-button {
|
|
||||||
margin: var(--space-small) auto 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<script>
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import MacroPreview from './MacroPreview.vue';
|
import { useStore } from 'dashboard/composables/store';
|
||||||
import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||||
import { useTrack } from 'dashboard/composables';
|
import { useTrack } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
components: {
|
import MacroPreview from './MacroPreview.vue';
|
||||||
MacroPreview,
|
|
||||||
},
|
const props = defineProps({
|
||||||
props: {
|
|
||||||
macro: {
|
macro: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -17,59 +18,61 @@ export default {
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
data() {
|
|
||||||
return {
|
const store = useStore();
|
||||||
isExecuting: false,
|
const { t } = useI18n();
|
||||||
showPreview: false,
|
|
||||||
};
|
const isExecuting = ref(false);
|
||||||
},
|
const showPreview = ref(false);
|
||||||
methods: {
|
|
||||||
async executeMacro(macro) {
|
const executeMacro = async macro => {
|
||||||
try {
|
try {
|
||||||
this.isExecuting = true;
|
isExecuting.value = true;
|
||||||
await this.$store.dispatch('macros/execute', {
|
await store.dispatch('macros/execute', {
|
||||||
macroId: macro.id,
|
macroId: macro.id,
|
||||||
conversationIds: [this.conversationId],
|
conversationIds: [props.conversationId],
|
||||||
});
|
});
|
||||||
useTrack(CONVERSATION_EVENTS.EXECUTED_A_MACRO);
|
useTrack(CONVERSATION_EVENTS.EXECUTED_A_MACRO);
|
||||||
useAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
|
useAlert(t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
useAlert(this.$t('MACROS.ERROR'));
|
useAlert(t('MACROS.ERROR'));
|
||||||
} finally {
|
} finally {
|
||||||
this.isExecuting = false;
|
isExecuting.value = false;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
toggleMacroPreview() {
|
|
||||||
this.showPreview = !this.showPreview;
|
const toggleMacroPreview = () => {
|
||||||
},
|
showPreview.value = !showPreview.value;
|
||||||
closeMacroPreview() {
|
};
|
||||||
this.showPreview = false;
|
|
||||||
},
|
const closeMacroPreview = () => {
|
||||||
},
|
showPreview.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="macro button secondary clear">
|
<div
|
||||||
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
|
class="relative flex items-center justify-between leading-4 rounded-md button secondary clear"
|
||||||
macro.name
|
>
|
||||||
}}</span>
|
<span class="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
<div class="flex items-center gap-1 macros-actions">
|
{{ macro.name }}
|
||||||
<woot-button
|
</span>
|
||||||
|
<div class="flex items-center gap-1 justify-end">
|
||||||
|
<NextButton
|
||||||
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
|
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
|
||||||
size="tiny"
|
icon="i-lucide-info"
|
||||||
variant="smooth"
|
slate
|
||||||
color-scheme="secondary"
|
faded
|
||||||
icon="info"
|
xs
|
||||||
@click="toggleMacroPreview(macro)"
|
@click="toggleMacroPreview"
|
||||||
/>
|
/>
|
||||||
<woot-button
|
<NextButton
|
||||||
v-tooltip.left-start="$t('MACROS.EXECUTE.BUTTON_TOOLTIP')"
|
v-tooltip.left-start="$t('MACROS.EXECUTE.BUTTON_TOOLTIP')"
|
||||||
size="tiny"
|
icon="i-lucide-play"
|
||||||
variant="smooth"
|
slate
|
||||||
color-scheme="secondary"
|
faded
|
||||||
icon="play-circle"
|
xs
|
||||||
:is-loading="isExecuting"
|
:is-loading="isExecuting"
|
||||||
@click="executeMacro(macro)"
|
@click="executeMacro(macro)"
|
||||||
/>
|
/>
|
||||||
@@ -83,13 +86,3 @@ export default {
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
|
||||||
|
|||||||
@@ -1,44 +1,30 @@
|
|||||||
<script>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
import {
|
import {
|
||||||
resolveActionName,
|
resolveActionName,
|
||||||
resolveTeamIds,
|
resolveTeamIds,
|
||||||
resolveLabels,
|
resolveLabels,
|
||||||
resolveAgents,
|
resolveAgents,
|
||||||
} from 'dashboard/routes/dashboard/settings/macros/macroHelper';
|
} from 'dashboard/routes/dashboard/settings/macros/macroHelper';
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
|
||||||
macro: {
|
macro: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
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({
|
const labels = useMapGetter('labels/getLabels');
|
||||||
labels: 'labels/getLabels',
|
const teams = useMapGetter('teams/getTeams');
|
||||||
teams: 'teams/getTeams',
|
const agents = useMapGetter('agents/getAgents');
|
||||||
agents: 'agents/getAgents',
|
|
||||||
}),
|
const getActionValue = (key, params) => {
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getActionValue(key, params) {
|
|
||||||
const actionsMap = {
|
const actionsMap = {
|
||||||
assign_team: resolveTeamIds(this.teams, params),
|
assign_team: resolveTeamIds(teams.value, params),
|
||||||
add_label: resolveLabels(this.labels, params),
|
add_label: resolveLabels(labels.value, params),
|
||||||
remove_label: resolveLabels(this.labels, params),
|
remove_label: resolveLabels(labels.value, params),
|
||||||
assign_agent: resolveAgents(this.agents, params),
|
assign_agent: resolveAgents(agents.value, params),
|
||||||
mute_conversation: null,
|
mute_conversation: null,
|
||||||
snooze_conversation: null,
|
snooze_conversation: null,
|
||||||
resolve_conversation: null,
|
resolve_conversation: null,
|
||||||
@@ -49,9 +35,14 @@ export default {
|
|||||||
add_private_note: params[0],
|
add_private_note: params[0],
|
||||||
};
|
};
|
||||||
return actionsMap[key] || '';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user