mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
feat: Update the design for macros design page (#9999)
This is the continuation of the design update for settings page. This PR updates the design for the macros page. Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ const FEATURE_HELP_URLS = {
|
|||||||
help_center: 'https://chwt.app/hc/help-center',
|
help_center: 'https://chwt.app/hc/help-center',
|
||||||
integrations: 'https://chwt.app/hc/integrations',
|
integrations: 'https://chwt.app/hc/integrations',
|
||||||
labels: 'https://chwt.app/hc/labels',
|
labels: 'https://chwt.app/hc/labels',
|
||||||
|
macros: 'https://chwt.app/hc/macros',
|
||||||
message_reply_to: 'https://chwt.app/hc/reply-to',
|
message_reply_to: 'https://chwt.app/hc/reply-to',
|
||||||
reports: 'https://chwt.app/hc/reports',
|
reports: 'https://chwt.app/hc/reports',
|
||||||
sla: 'https://chwt.app/hc/sla',
|
sla: 'https://chwt.app/hc/sla',
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"MACROS": {
|
"MACROS": {
|
||||||
"HEADER": "Macros",
|
"HEADER": "Macros",
|
||||||
|
"DESCRIPTION": "A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click.",
|
||||||
|
"LEARN_MORE": "Learn more about macros",
|
||||||
"HEADER_BTN_TXT": "Add a new macro",
|
"HEADER_BTN_TXT": "Add a new macro",
|
||||||
"HEADER_BTN_TXT_SAVE": "Save macro",
|
"HEADER_BTN_TXT_SAVE": "Save macro",
|
||||||
"LOADING": "Fetching macros",
|
"LOADING": "Fetching macros",
|
||||||
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
|
|
||||||
"ERROR": "Something went wrong. Please try again",
|
"ERROR": "Something went wrong. Please try again",
|
||||||
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
|
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
|
||||||
"ADD": {
|
"ADD": {
|
||||||
@@ -24,7 +25,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"TABLE_HEADER": ["Name", "Created by", "Last updated by", "Visibility"],
|
"TABLE_HEADER": [
|
||||||
|
"Name",
|
||||||
|
"Created by",
|
||||||
|
"Last updated by",
|
||||||
|
"Visibility"
|
||||||
|
],
|
||||||
"404": "No macros found"
|
"404": "No macros found"
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
|
|||||||
@@ -1,117 +1,112 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
|
||||||
import MacrosTableRow from './MacrosTableRow.vue';
|
import MacrosTableRow from './MacrosTableRow.vue';
|
||||||
export default {
|
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||||
components: {
|
import SettingsLayout from '../SettingsLayout.vue';
|
||||||
MacrosTableRow,
|
import { computed, onMounted, ref } from 'vue';
|
||||||
},
|
import { useI18n } from 'dashboard/composables/useI18n';
|
||||||
setup() {
|
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||||
const { accountScopedUrl } = useAccount();
|
|
||||||
return {
|
const getters = useStoreGetters();
|
||||||
accountScopedUrl,
|
const store = useStore();
|
||||||
};
|
const { t } = useI18n();
|
||||||
},
|
|
||||||
data() {
|
const showDeleteConfirmationPopup = ref(false);
|
||||||
return {
|
const selectedMacro = ref({});
|
||||||
showDeleteConfirmationPopup: false,
|
|
||||||
selectedResponse: {},
|
const records = computed(() => getters['macros/getMacros'].value);
|
||||||
loading: {},
|
const uiFlags = computed(() => getters['macros/getUIFlags'].value);
|
||||||
};
|
|
||||||
},
|
const deleteMessage = computed(() => ` ${selectedMacro.value.name}?`);
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
onMounted(() => {
|
||||||
records: ['macros/getMacros'],
|
store.dispatch('macros/get');
|
||||||
uiFlags: 'macros/getUIFlags',
|
});
|
||||||
}),
|
|
||||||
deleteMessage() {
|
const deleteMacro = async id => {
|
||||||
return ` ${this.selectedResponse.name}?`;
|
try {
|
||||||
},
|
await store.dispatch('macros/delete', id);
|
||||||
},
|
useAlert(t('MACROS.DELETE.API.SUCCESS_MESSAGE'));
|
||||||
mounted() {
|
} catch (error) {
|
||||||
this.$store.dispatch('macros/get');
|
useAlert(t('MACROS.DELETE.API.ERROR_MESSAGE'));
|
||||||
},
|
}
|
||||||
methods: {
|
};
|
||||||
openDeletePopup(response) {
|
|
||||||
this.showDeleteConfirmationPopup = true;
|
const openDeletePopup = response => {
|
||||||
this.selectedResponse = response;
|
showDeleteConfirmationPopup.value = true;
|
||||||
},
|
selectedMacro.value = response;
|
||||||
closeDeletePopup() {
|
};
|
||||||
this.showDeleteConfirmationPopup = false;
|
|
||||||
},
|
const closeDeletePopup = () => {
|
||||||
confirmDeletion() {
|
showDeleteConfirmationPopup.value = false;
|
||||||
this.loading[this.selectedResponse.id] = true;
|
};
|
||||||
this.closeDeletePopup();
|
|
||||||
this.deleteMacro(this.selectedResponse.id);
|
const confirmDeletion = () => {
|
||||||
},
|
closeDeletePopup();
|
||||||
async deleteMacro(id) {
|
deleteMacro(selectedMacro.value.id);
|
||||||
try {
|
|
||||||
await this.$store.dispatch('macros/delete', id);
|
|
||||||
useAlert(this.$t('MACROS.DELETE.API.SUCCESS_MESSAGE'));
|
|
||||||
this.loading[this.selectedResponse.id] = false;
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(this.$t('MACROS.DELETE.API.ERROR_MESSAGE'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-1 overflow-auto">
|
<SettingsLayout
|
||||||
<router-link
|
:no-records-message="$t('MACROS.LIST.404')"
|
||||||
:to="accountScopedUrl('settings/macros/new')"
|
:no-records-found="!records.length"
|
||||||
class="button success button--fixed-top button success button--fixed-top px-3.5 py-1 rounded-[5px] flex gap-2"
|
:is-loading="uiFlags.isFetching"
|
||||||
>
|
:loading-message="$t('MACROS.LOADING')"
|
||||||
<fluent-icon icon="add-circle" />
|
feature-name="macros"
|
||||||
<span class="button__content">
|
>
|
||||||
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
<template #header>
|
||||||
</span>
|
<BaseSettingsHeader
|
||||||
</router-link>
|
:title="$t('MACROS.HEADER')"
|
||||||
<div class="flex flex-row gap-4 p-8">
|
:description="$t('MACROS.DESCRIPTION')"
|
||||||
<div class="w-full lg:w-3/5">
|
:link-text="$t('MACROS.LEARN_MORE')"
|
||||||
<div v-if="!uiFlags.isFetching && !records.length" class="p-3">
|
feature-name="macros"
|
||||||
<p class="flex flex-col items-center justify-center h-full">
|
>
|
||||||
{{ $t('MACROS.LIST.404') }}
|
<template #actions>
|
||||||
</p>
|
<router-link
|
||||||
</div>
|
:to="{ name: 'macros_new' }"
|
||||||
<woot-loading-state
|
class="button rounded-md primary"
|
||||||
v-if="uiFlags.isFetching"
|
>
|
||||||
:message="$t('MACROS.LOADING')"
|
<fluent-icon icon="add-circle" />
|
||||||
/>
|
<span class="button__content">
|
||||||
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
|
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
||||||
<thead>
|
</span>
|
||||||
<th
|
</router-link>
|
||||||
v-for="thHeader in $t('MACROS.LIST.TABLE_HEADER')"
|
</template>
|
||||||
:key="thHeader"
|
</BaseSettingsHeader>
|
||||||
>
|
</template>
|
||||||
{{ thHeader }}
|
<template #body>
|
||||||
</th>
|
<table class="min-w-full divide-y divide-slate-75 dark:divide-slate-700">
|
||||||
</thead>
|
<thead>
|
||||||
<tbody>
|
<th
|
||||||
<MacrosTableRow
|
v-for="thHeader in $t('MACROS.LIST.TABLE_HEADER')"
|
||||||
v-for="(macro, index) in records"
|
:key="thHeader"
|
||||||
:key="index"
|
class="py-4 ltr:pr-4 rtl:pl-4 text-left font-semibold text-slate-700 dark:text-slate-300"
|
||||||
:macro="macro"
|
>
|
||||||
@delete="openDeletePopup(macro, index)"
|
{{ thHeader }}
|
||||||
/>
|
</th>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
<tbody
|
||||||
</div>
|
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
|
||||||
<div class="hidden w-1/3 lg:block">
|
>
|
||||||
<span v-dompurify-html="$t('MACROS.SIDEBAR_TXT')" />
|
<MacrosTableRow
|
||||||
</div>
|
v-for="(macro, index) in records"
|
||||||
</div>
|
:key="index"
|
||||||
<woot-delete-modal
|
:macro="macro"
|
||||||
:show.sync="showDeleteConfirmationPopup"
|
@delete="openDeletePopup(macro)"
|
||||||
:on-close="closeDeletePopup"
|
/>
|
||||||
:on-confirm="confirmDeletion"
|
</tbody>
|
||||||
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
</table>
|
||||||
:message="$t('MACROS.DELETE.CONFIRM.MESSAGE')"
|
<woot-delete-modal
|
||||||
:message-value="deleteMessage"
|
:show.sync="showDeleteConfirmationPopup"
|
||||||
:confirm-text="$t('MACROS.DELETE.CONFIRM.YES')"
|
:on-close="closeDeletePopup"
|
||||||
:reject-text="$t('MACROS.DELETE.CONFIRM.NO')"
|
:on-confirm="confirmDeletion"
|
||||||
/>
|
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||||
</div>
|
:message="$t('MACROS.DELETE.CONFIRM.MESSAGE')"
|
||||||
|
:message-value="deleteMessage"
|
||||||
|
:confirm-text="$t('MACROS.DELETE.CONFIRM.YES')"
|
||||||
|
:reject-text="$t('MACROS.DELETE.CONFIRM.NO')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</SettingsLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,59 +1,56 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { computed } from 'vue';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
export default {
|
import { useI18n } from 'dashboard/composables/useI18n';
|
||||||
components: {
|
|
||||||
Thumbnail,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
macro: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { accountScopedUrl } = useAccount();
|
|
||||||
|
|
||||||
return {
|
const props = defineProps({
|
||||||
accountScopedUrl,
|
macro: {
|
||||||
};
|
type: Object,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
computed: {
|
});
|
||||||
createdByName() {
|
defineEmits(['delete']);
|
||||||
const createdBy = this.macro.created_by;
|
const { t } = useI18n();
|
||||||
return createdBy.available_name ?? createdBy.email ?? '';
|
|
||||||
},
|
const createdByName = computed(() => {
|
||||||
updatedByName() {
|
const createdBy = props.macro.created_by;
|
||||||
const updatedBy = this.macro.updated_by;
|
return createdBy.available_name ?? createdBy.email ?? '';
|
||||||
return updatedBy.available_name ?? updatedBy.email ?? '';
|
});
|
||||||
},
|
|
||||||
visibilityLabel() {
|
const updatedByName = computed(() => {
|
||||||
return this.macro.visibility === 'global'
|
const updatedBy = props.macro.updated_by;
|
||||||
? this.$t('MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL')
|
return updatedBy.available_name ?? updatedBy.email ?? '';
|
||||||
: this.$t('MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL');
|
});
|
||||||
},
|
|
||||||
},
|
const visibilityLabel = computed(() => {
|
||||||
};
|
const i18nKey =
|
||||||
|
props.macro.visibility === 'global'
|
||||||
|
? 'MACROS.EDITOR.VISIBILITY.GLOBAL.LABEL'
|
||||||
|
: 'MACROS.EDITOR.VISIBILITY.PERSONAL.LABEL';
|
||||||
|
return t(i18nKey);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ macro.name }}</td>
|
<td class="py-4 ltr:pr-4 rtl:pl-4 truncate">{{ macro.name }}</td>
|
||||||
<td>
|
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||||
<div v-if="macro.created_by" class="avatar-container">
|
<div v-if="macro.created_by" class="flex items-center">
|
||||||
<Thumbnail :username="createdByName" size="24px" />
|
<Thumbnail :username="createdByName" size="24px" />
|
||||||
<span>{{ createdByName }}</span>
|
<span class="mx-2">{{ createdByName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>--</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||||
<div v-if="macro.updated_by" class="avatar-container">
|
<div v-if="macro.updated_by" class="flex items-center">
|
||||||
<Thumbnail :username="updatedByName" size="24px" />
|
<Thumbnail :username="updatedByName" size="24px" />
|
||||||
<span>{{ updatedByName }}</span>
|
<span class="mx-2">{{ updatedByName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>--</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ visibilityLabel }}</td>
|
<td class="py-4 ltr:pr-4 rtl:pl-4">{{ visibilityLabel }}</td>
|
||||||
<td class="button-wrapper">
|
<td class="py-4 flex justify-end gap-1">
|
||||||
<router-link :to="accountScopedUrl(`settings/macros/${macro.id}/edit`)">
|
<router-link :to="{ name: 'macros_edit', params: { macroId: macro.id } }">
|
||||||
<woot-button
|
<woot-button
|
||||||
v-tooltip.top="$t('MACROS.EDIT.TOOLTIP')"
|
v-tooltip.top="$t('MACROS.EDIT.TOOLTIP')"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
@@ -75,15 +72,3 @@ export default {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.avatar-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-left: var(--space-small);
|
|
||||||
margin-right: var(--space-small);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
|
|
||||||
const SettingsContent = () => import('../Wrapper.vue');
|
const SettingsContent = () => import('../Wrapper.vue');
|
||||||
|
const SettingsWrapper = () => import('../SettingsWrapper.vue');
|
||||||
const Macros = () => import('./Index.vue');
|
const Macros = () => import('./Index.vue');
|
||||||
const MacroEditor = () => import('./MacroEditor.vue');
|
const MacroEditor = () => import('./MacroEditor.vue');
|
||||||
|
|
||||||
@@ -8,16 +9,7 @@ export default {
|
|||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/settings/macros'),
|
path: frontendURL('accounts/:accountId/settings/macros'),
|
||||||
component: SettingsContent,
|
component: SettingsWrapper,
|
||||||
props: params => {
|
|
||||||
const showBackButton = params.name !== 'macros_wrapper';
|
|
||||||
return {
|
|
||||||
headerTitle: 'MACROS.HEADER',
|
|
||||||
headerButtonText: 'MACROS.HEADER_BTN_TXT',
|
|
||||||
icon: 'flash-settings',
|
|
||||||
showBackButton,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@@ -27,6 +19,27 @@ export default {
|
|||||||
permissions: ['administrator', 'agent'],
|
permissions: ['administrator', 'agent'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/settings/macros'),
|
||||||
|
component: SettingsContent,
|
||||||
|
props: () => {
|
||||||
|
return {
|
||||||
|
headerTitle: 'MACROS.HEADER',
|
||||||
|
icon: 'flash-settings',
|
||||||
|
showBackButton: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':macroId/edit',
|
||||||
|
name: 'macros_edit',
|
||||||
|
component: MacroEditor,
|
||||||
|
meta: {
|
||||||
|
permissions: ['administrator', 'agent'],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'new',
|
path: 'new',
|
||||||
name: 'macros_new',
|
name: 'macros_new',
|
||||||
@@ -35,14 +48,6 @@ export default {
|
|||||||
permissions: ['administrator', 'agent'],
|
permissions: ['administrator', 'agent'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ':macroId/edit',
|
|
||||||
name: 'macros_edit',
|
|
||||||
component: MacroEditor,
|
|
||||||
meta: {
|
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user