feat: Update the design for automation (#10002)

This is the continuation of the design update for the settings screens.
In this PR, the automation page is updated with the latest design.

- Moved the row to a new component
- Migrated both components to composition API.
- Order by ID (Earlier this was order by updated_at which was
confusing).

| Light | Dark |
| --  | -- |
| <img width="1438" alt="Screenshot 2024-08-21 at 9 46 48 PM"
src="https://github.com/user-attachments/assets/89d96745-6556-48a1-82fa-a115325c24c0">
| <img width="1398" alt="Screenshot 2024-08-21 at 9 46 57 PM"
src="https://github.com/user-attachments/assets/5f1935ec-6d0e-4f82-b895-f47244764474">
|

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
Pranav
2024-08-22 16:22:54 +05:30
committed by GitHub
parent eb6de74269
commit 4aa4e2549f
5 changed files with 298 additions and 245 deletions

View File

@@ -1,9 +1,10 @@
{
"AUTOMATION": {
"HEADER": "Automations",
"HEADER": "Automation",
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
"LEARN_MORE": "Learn more about automation",
"HEADER_BTN_TXT": "Add Automation Rule",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
"ADD": {
"TITLE": "Add Automation Rule",
"SUBMIT": "Create",
@@ -39,7 +40,12 @@
}
},
"LIST": {
"TABLE_HEADER": ["Name", "Description", "Active", "Created on"],
"TABLE_HEADER": [
"Name",
"Description",
"Active",
"Created on"
],
"404": "No automation rules found"
},
"DELETE": {

View File

@@ -0,0 +1,79 @@
<script setup>
import { messageStamp } from 'shared/helpers/timeHelper';
const props = defineProps({
automation: {
type: Object,
required: true,
},
loading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['toggle', 'edit', 'delete', 'clone']);
const readableDate = date => messageStamp(new Date(date), 'LLL d, yyyy');
const readableDateWithTime = date =>
messageStamp(new Date(date), 'LLL d, yyyy hh:mm a');
const toggle = () => {
const { id, name, active } = props.automation;
emit('toggle', {
id,
name,
status: active,
});
};
</script>
<template>
<tr>
<td class="py-4 ltr:pr-4 rtl:pl-4 min-w-[200px]">{{ automation.name }}</td>
<td class="py-4 ltr:pr-4 rtl:pl-4">{{ automation.description }}</td>
<td class="py-4 ltr:pr-4 rtl:pl-4">
<woot-switch :value="automation.active" @input="toggle" />
</td>
<td
class="py-4 ltr:pr-4 rtl:pl-4 min-w-[12px]"
:title="readableDateWithTime(automation.created_on)"
>
{{ readableDate(automation.created_on) }}
</td>
<td class="py-4 min-w-xs">
<div class="flex gap-1 justify-end flex-shrink-0">
<woot-button
v-tooltip.top="$t('AUTOMATION.FORM.EDIT')"
variant="smooth"
size="tiny"
color-scheme="secondary"
class-names="grey-btn"
icon="edit"
:is-loading="loading"
@click="$emit('edit', automation)"
/>
<woot-button
v-tooltip.top="$t('AUTOMATION.CLONE.TOOLTIP')"
variant="smooth"
size="tiny"
:is-loading="loading"
color-scheme="primary"
class-names="grey-btn"
icon="copy"
@click="$emit('clone', automation)"
/>
<woot-button
v-tooltip.top="$t('AUTOMATION.FORM.DELETE')"
variant="smooth"
:is-loading="loading"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
class-names="grey-btn"
@click="$emit('delete', automation)"
/>
</div>
</td>
</tr>
</template>

View File

@@ -1,246 +1,219 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { useAlert } from 'dashboard/composables';
import { messageStamp } from 'shared/helpers/timeHelper';
import AddAutomationRule from './AddAutomationRule.vue';
import EditAutomationRule from './EditAutomationRule.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import SettingsLayout from '../SettingsLayout.vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import AutomationRuleRow from './AutomationRuleRow.vue';
const getters = useStoreGetters();
const store = useStore();
const { t } = useI18n();
const confirmDialog = ref(null);
export default {
components: {
AddAutomationRule,
EditAutomationRule,
},
data() {
return {
loading: {},
showAddPopup: false,
showEditPopup: false,
showDeleteConfirmationPopup: false,
selectedResponse: {},
toggleModalTitle: this.$t('AUTOMATION.TOGGLE.ACTIVATION_TITLE'),
toggleModalDescription: this.$t(
'AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION'
),
};
},
computed: {
...mapGetters({
records: ['automations/getAutomations'],
uiFlags: 'automations/getUIFlags',
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('AUTOMATION.DELETE.CONFIRM.YES')} ${
this.selectedResponse.name
}`;
},
deleteRejectText() {
return `${this.$t('AUTOMATION.DELETE.CONFIRM.NO')} ${
this.selectedResponse.name
}`;
},
deleteMessage() {
return ` ${this.selectedResponse.name}?`;
},
isSLAEnabled() {
return this.isFeatureEnabledonAccount(this.accountId, 'sla');
},
},
mounted() {
this.$store.dispatch('inboxes/get');
this.$store.dispatch('agents/get');
this.$store.dispatch('contacts/get');
this.$store.dispatch('teams/get');
this.$store.dispatch('labels/get');
this.$store.dispatch('campaigns/get');
this.$store.dispatch('automations/get');
if (this.isSLAEnabled) this.$store.dispatch('sla/get');
},
methods: {
openAddPopup() {
this.showAddPopup = true;
},
hideAddPopup() {
this.showAddPopup = false;
},
openEditPopup(response) {
this.selectedResponse = response;
this.showEditPopup = true;
},
hideEditPopup() {
this.showEditPopup = false;
},
openDeletePopup(response) {
this.showDeleteConfirmationPopup = true;
this.selectedResponse = response;
},
closeDeletePopup() {
this.showDeleteConfirmationPopup = false;
},
confirmDeletion() {
this.loading[this.selectedResponse.id] = true;
this.closeDeletePopup();
this.deleteAutomation(this.selectedResponse.id);
},
async deleteAutomation(id) {
try {
await this.$store.dispatch('automations/delete', id);
useAlert(this.$t('AUTOMATION.DELETE.API.SUCCESS_MESSAGE'));
this.loading[this.selectedResponse.id] = false;
} catch (error) {
useAlert(this.$t('AUTOMATION.DELETE.API.ERROR_MESSAGE'));
}
},
async cloneAutomation(id) {
try {
await this.$store.dispatch('automations/clone', id);
useAlert(this.$t('AUTOMATION.CLONE.API.SUCCESS_MESSAGE'));
this.$store.dispatch('automations/get');
this.loading[this.selectedResponse.id] = false;
} catch (error) {
useAlert(this.$t('AUTOMATION.CLONE.API.ERROR_MESSAGE'));
}
},
async submitAutomation(payload, mode) {
try {
const action =
mode === 'edit' ? 'automations/update' : 'automations/create';
const successMessage =
mode === 'edit'
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
await this.$store.dispatch(action, payload);
useAlert(successMessage);
this.hideAddPopup();
this.hideEditPopup();
} catch (error) {
const errorMessage =
mode === 'edit'
? this.$t('AUTOMATION.EDIT.API.ERROR_MESSAGE')
: this.$t('AUTOMATION.ADD.API.ERROR_MESSAGE');
useAlert(errorMessage);
}
},
async toggleAutomation(automation, status) {
try {
this.toggleModalTitle = status
? this.$t('AUTOMATION.TOGGLE.DEACTIVATION_TITLE')
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_TITLE');
this.toggleModalDescription = status
? this.$t('AUTOMATION.TOGGLE.DEACTIVATION_DESCRIPTION', {
automationName: automation.name,
})
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION', {
automationName: automation.name,
});
// Check if user confirms to proceed
const ok = await this.$refs.confirmDialog.showConfirmation();
if (ok) {
await await this.$store.dispatch('automations/update', {
id: automation.id,
active: !status,
});
const message = status
? this.$t('AUTOMATION.TOGGLE.DEACTIVATION_SUCCESFUL')
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_SUCCESFUL');
useAlert(message);
const loading = ref({});
const showAddPopup = ref(false);
const showEditPopup = ref(false);
const showDeleteConfirmationPopup = ref(false);
const selectedAutomation = ref({});
const toggleModalTitle = ref(t('AUTOMATION.TOGGLE.ACTIVATION_TITLE'));
const toggleModalDescription = ref(
t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION')
);
const records = computed(() => getters['automations/getAutomations'].value);
const uiFlags = computed(() => getters['automations/getUIFlags'].value);
const accountId = computed(() => getters.getCurrentAccountId.value);
const deleteConfirmText = computed(
() => `${t('AUTOMATION.DELETE.CONFIRM.YES')} ${selectedAutomation.value.name}`
);
const deleteRejectText = computed(
() => `${t('AUTOMATION.DELETE.CONFIRM.NO')} ${selectedAutomation.value.name}`
);
const deleteMessage = computed(() => ` ${selectedAutomation.value.name}?`);
const isSLAEnabled = computed(() =>
getters['accounts/isFeatureEnabledonAccount'].value(accountId.value, 'sla')
);
onMounted(() => {
store.dispatch('inboxes/get');
store.dispatch('agents/get');
store.dispatch('contacts/get');
store.dispatch('teams/get');
store.dispatch('labels/get');
store.dispatch('campaigns/get');
store.dispatch('automations/get');
if (isSLAEnabled.value) {
store.dispatch('sla/get');
}
});
const openAddPopup = () => {
showAddPopup.value = true;
};
const hideAddPopup = () => {
showAddPopup.value = false;
};
const openEditPopup = response => {
selectedAutomation.value = response;
showEditPopup.value = true;
};
const hideEditPopup = () => {
showEditPopup.value = false;
};
const openDeletePopup = response => {
showDeleteConfirmationPopup.value = true;
selectedAutomation.value = response;
};
const closeDeletePopup = () => {
showDeleteConfirmationPopup.value = false;
};
const deleteAutomation = async id => {
try {
await store.dispatch('automations/delete', id);
useAlert(t('AUTOMATION.DELETE.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(t('AUTOMATION.DELETE.API.ERROR_MESSAGE'));
} finally {
loading.value[selectedAutomation.value.id] = false;
}
};
const confirmDeletion = () => {
loading.value[selectedAutomation.value.id] = true;
closeDeletePopup();
deleteAutomation(selectedAutomation.value.id);
};
const cloneAutomation = async ({ id }) => {
try {
await store.dispatch('automations/clone', id);
useAlert(t('AUTOMATION.CLONE.API.SUCCESS_MESSAGE'));
store.dispatch('automations/get');
} catch (error) {
useAlert(t('AUTOMATION.CLONE.API.ERROR_MESSAGE'));
} finally {
loading.value[selectedAutomation.value.id] = false;
}
};
const submitAutomation = async (payload, mode) => {
try {
const action =
mode === 'edit' ? 'automations/update' : 'automations/create';
const successMessage =
mode === 'edit'
? t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
: t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
await store.dispatch(action, payload);
useAlert(successMessage);
hideAddPopup();
hideEditPopup();
} catch (error) {
const errorMessage =
mode === 'edit'
? t('AUTOMATION.EDIT.API.ERROR_MESSAGE')
: t('AUTOMATION.ADD.API.ERROR_MESSAGE');
useAlert(errorMessage);
}
};
const toggleAutomation = async ({ id, name, status }) => {
try {
if (status) {
toggleModalTitle.value = t('AUTOMATION.TOGGLE.DEACTIVATION_TITLE');
toggleModalDescription.value = t(
'AUTOMATION.TOGGLE.DEACTIVATION_DESCRIPTION',
{
automationName: name,
}
} catch (error) {
useAlert(this.$t('AUTOMATION.EDIT.API.ERROR_MESSAGE'));
}
},
readableTime(date) {
return messageStamp(new Date(date), 'LLL d, h:mm a');
},
},
);
} else {
toggleModalTitle.value = t('AUTOMATION.TOGGLE.ACTIVATION_TITLE');
toggleModalDescription.value = t(
'AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION',
{
automationName: name,
}
);
}
const ok = await confirmDialog.value.showConfirmation();
if (ok) {
await store.dispatch('automations/update', {
id: id,
active: !status,
});
const message = status
? t('AUTOMATION.TOGGLE.DEACTIVATION_SUCCESFUL')
: t('AUTOMATION.TOGGLE.ACTIVATION_SUCCESFUL');
useAlert(message);
}
} catch (error) {
useAlert(t('AUTOMATION.EDIT.API.ERROR_MESSAGE'));
}
};
</script>
<template>
<div class="flex-1 p-4 overflow-auto">
<woot-button
color-scheme="success"
class-names="button--fixed-top"
icon="add-circle"
@click="openAddPopup()"
>
{{ $t('AUTOMATION.HEADER_BTN_TXT') }}
</woot-button>
<div class="flex flex-row gap-4">
<div class="w-full lg:w-3/5">
<p
v-if="!uiFlags.isFetching && !records.length"
class="flex flex-col items-center justify-center h-full"
<SettingsLayout
:is-loading="uiFlags.isFetching"
:loading-message="$t('AUTOMATION.LOADING')"
:no-records-found="!records.length"
:no-records-message="$t('AUTOMATION.LIST.404')"
>
<template #header>
<BaseSettingsHeader
:title="$t('AUTOMATION.HEADER')"
:description="$t('AUTOMATION.DESCRIPTION')"
:link-text="$t('AUTOMATION.LEARN_MORE')"
feature-name="automation"
>
<template #actions>
<woot-button
class="button nice rounded-md"
icon="add-circle"
@click="openAddPopup"
>
{{ $t('AUTOMATION.HEADER_BTN_TXT') }}
</woot-button>
</template>
</BaseSettingsHeader>
</template>
<template #body>
<table class="min-w-full divide-y divide-slate-75 dark:divide-slate-700">
<thead>
<th
v-for="thHeader in $t('AUTOMATION.LIST.TABLE_HEADER')"
:key="thHeader"
class="py-4 pr-4 text-left font-semibold text-slate-700 dark:text-slate-300"
>
{{ thHeader }}
</th>
</thead>
<tbody
class="divide-y divide-slate-50 dark:divide-slate-800 text-slate-700 dark:text-slate-300"
>
{{ $t('AUTOMATION.LIST.404') }}
</p>
<woot-loading-state
v-if="uiFlags.isFetching"
:message="$t('AUTOMATION.LOADING')"
/>
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
<thead>
<th
v-for="thHeader in $t('AUTOMATION.LIST.TABLE_HEADER')"
:key="thHeader"
>
{{ thHeader }}
</th>
</thead>
<tbody>
<tr v-for="(automation, index) in records" :key="index">
<td>{{ automation.name }}</td>
<td>{{ automation.description }}</td>
<td>
<woot-switch
:value="automation.active"
@input="toggleAutomation(automation, automation.active)"
/>
</td>
<td>{{ readableTime(automation.created_on) }}</td>
<td class="button-wrapper">
<woot-button
v-tooltip.top="$t('AUTOMATION.FORM.EDIT')"
variant="smooth"
size="tiny"
color-scheme="secondary"
class-names="grey-btn"
:is-loading="loading[automation.id]"
icon="edit"
@click="openEditPopup(automation)"
/>
<woot-button
v-tooltip.top="$t('AUTOMATION.CLONE.TOOLTIP')"
variant="smooth"
size="tiny"
color-scheme="primary"
class-names="grey-btn"
:is-loading="loading[automation.id]"
icon="copy"
@click="cloneAutomation(automation.id)"
/>
<woot-button
v-tooltip.top="$t('AUTOMATION.FORM.DELETE')"
variant="smooth"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
class-names="grey-btn"
:is-loading="loading[automation.id]"
@click="openDeletePopup(automation, index)"
/>
</td>
</tr>
</tbody>
</table>
</div>
<AutomationRuleRow
v-for="automation in records"
:key="automation.id"
:automation="automation"
:loading="loading[automation.id]"
@clone="cloneAutomation"
@toggle="toggleAutomation"
@edit="openEditPopup"
@delete="openDeletePopup"
/>
</tbody>
</table>
</template>
<div class="hidden w-1/3 lg:block">
<span v-dompurify-html="$t('AUTOMATION.SIDEBAR_TXT')" />
</div>
</div>
<woot-modal
:show.sync="showAddPopup"
size="medium"
@@ -272,7 +245,7 @@ export default {
<EditAutomationRule
v-if="showEditPopup"
:on-close="hideEditPopup"
:selected-response="selectedResponse"
:selected-response="selectedAutomation"
@saveAutomation="submitAutomation"
/>
</woot-modal>
@@ -281,5 +254,5 @@ export default {
:title="toggleModalTitle"
:description="toggleModalDescription"
/>
</div>
</SettingsLayout>
</template>

View File

@@ -1,17 +1,12 @@
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const SettingsWrapper = () => import('../SettingsWrapper.vue');
const Automation = () => import('./Index.vue');
export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/automation'),
component: SettingsContent,
props: {
headerTitle: 'AUTOMATION.HEADER',
icon: 'automation',
showNewButton: false,
},
component: SettingsWrapper,
children: [
{
path: '',

View File

@@ -15,7 +15,7 @@ export const state = {
export const getters = {
getAutomations(_state) {
return _state.records;
return _state.records.sort((a1, a2) => a1.id - a2.id);
},
getUIFlags(_state) {
return _state.uiFlags;