feat: Update the design for the Inbox management console (#10043)

This is the continuation of the design update PR. This changes the design for the inbox pages.
---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Pranav
2024-08-29 11:19:32 +05:30
committed by GitHub
parent 3b5f5b41ad
commit 31e7663258
5 changed files with 244 additions and 211 deletions

View File

@@ -9,6 +9,7 @@ const FEATURE_HELP_URLS = {
custom_attributes: 'https://chwt.app/hc/custom-attributes',
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
help_center: 'https://chwt.app/hc/help-center',
inboxes: 'https://chwt.app/hc/inboxes',
integrations: 'https://chwt.app/hc/integrations',
labels: 'https://chwt.app/hc/labels',
macros: 'https://chwt.app/hc/macros',

View File

@@ -1,7 +1,8 @@
{
"INBOX_MGMT": {
"HEADER": "Inboxes",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
"LEARN_MORE": "Learn more about inboxes",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
@@ -745,6 +746,18 @@
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
},
"CHANNELS": {
"MESSENGER": "Messenger",
"WEB_WIDGET": "Website",
"TWITTER_PROFILE": "Twitter",
"TWILIO_SMS": "Twilio SMS",
"WHATSAPP": "WhatsApp",
"SMS": "SMS",
"EMAIL": "Email",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel"
}
}
}

View File

@@ -1,219 +1,177 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { useAlert } from 'dashboard/composables';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useAccount } from 'dashboard/composables/useAccount';
import Settings from './Settings.vue';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import SettingsLayout from '../SettingsLayout.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import { computed, ref } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import ChannelName from './components/ChannelName.vue';
export default {
components: {
Settings,
},
mixins: [globalConfigMixin],
setup() {
const { isAdmin } = useAdmin();
const { accountScopedUrl } = useAccount();
return {
isAdmin,
accountScopedUrl,
};
},
data() {
return {
loading: {},
showSettings: false,
showDeletePopup: false,
selectedInbox: {},
};
},
computed: {
...mapGetters({
inboxesList: 'inboxes/getInboxes',
globalConfig: 'globalConfig/get',
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedInbox.name
}`;
},
deleteRejectText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedInbox.name
}`;
},
confirmDeleteMessage() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedInbox.name
}?`;
},
confirmPlaceHolderText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
inboxName: this.selectedInbox.name,
})}`;
},
},
methods: {
twilioChannelName(item) {
const { medium = '' } = item;
if (medium === 'whatsapp') return 'WhatsApp';
return 'Twilio SMS';
},
openSettings(inbox) {
this.showSettings = true;
this.selectedInbox = inbox;
},
closeSettings() {
this.showSettings = false;
this.selectedInbox = {};
},
async deleteInbox({ id }) {
try {
await this.$store.dispatch('inboxes/delete', id);
useAlert(this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
}
},
const getters = useStoreGetters();
const store = useStore();
const { t } = useI18n();
const { isAdmin } = useAdmin();
confirmDeletion() {
this.deleteInbox(this.selectedInbox);
this.closeDelete();
},
openDelete(inbox) {
this.showDeletePopup = true;
this.selectedInbox = inbox;
},
closeDelete() {
this.showDeletePopup = false;
this.selectedInbox = {};
},
},
const showDeletePopup = ref(false);
const selectedInbox = ref({});
const inboxesList = computed(() => getters['inboxes/getInboxes'].value);
const uiFlags = computed(() => getters['inboxes/getUIFlags'].value);
const deleteConfirmText = computed(
() => `${t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${selectedInbox.value.name}`
);
const deleteRejectText = computed(
() => `${t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${selectedInbox.value.name}`
);
const confirmDeleteMessage = computed(
() => `${t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${selectedInbox.value.name}?`
);
const confirmPlaceHolderText = computed(
() =>
`${t('INBOX_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
inboxName: selectedInbox.value.name,
})}`
);
const deleteInbox = async ({ id }) => {
try {
await store.dispatch('inboxes/delete', id);
useAlert(t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
}
};
const closeDelete = () => {
showDeletePopup.value = false;
selectedInbox.value = {};
};
const confirmDeletion = () => {
deleteInbox(selectedInbox.value);
closeDelete();
};
const openDelete = inbox => {
showDeletePopup.value = true;
selectedInbox.value = inbox;
};
</script>
<template>
<div class="flex-1 overflow-auto">
<div class="flex flex-row gap-4 p-8">
<div class="w-full lg:w-3/5">
<p
v-if="!inboxesList.length"
class="flex flex-col items-center justify-center h-full"
>
{{ $t('INBOX_MGMT.LIST.404') }}
<SettingsLayout
:no-records-found="!inboxesList.length"
:no-records-message="$t('INBOX_MGMT.LIST.404')"
:is-loading="uiFlags.isFetching"
>
<template #header>
<BaseSettingsHeader
:title="$t('INBOX_MGMT.HEADER')"
:description="$t('INBOX_MGMT.DESCRIPTION')"
:link-text="$t('INBOX_MGMT.LEARN_MORE')"
feature-name="inboxes"
>
<template #actions>
<router-link
v-if="isAdmin"
:to="accountScopedUrl('settings/inboxes/new')"
class="button nice rounded-md"
:to="{ name: 'settings_inbox_new' }"
>
<fluent-icon icon="add-circle" />
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
</router-link>
</p>
<table v-if="inboxesList.length" class="woot-table">
<tbody>
<tr v-for="item in inboxesList" :key="item.id">
<td>
<img
v-if="item.avatar_url"
class="woot-thumbnail"
:src="item.avatar_url"
alt="No Page Image"
</template>
</BaseSettingsHeader>
</template>
<template #body>
<table
class="min-w-full overflow-x-auto divide-y divide-slate-75 dark:divide-slate-700"
>
<tbody
class="divide-y divide-slate-25 dark:divide-slate-800 flex-1 text-slate-700 dark:text-slate-100"
>
<tr v-for="inbox in inboxesList" :key="inbox.id">
<td class="py-4 ltr:pr-4 rtl:pl-4">
<div class="flex items-center flex-row gap-4">
<Thumbnail
v-if="inbox.avatar_url"
class="bg-black-50 dark:bg-black-800 rounded-full p-2 ring ring-opacity-20 dark:ring-opacity-80 ring-black-100 dark:ring-black-900 border border-slate-100 dark:border-slate-700/50 shadow-sm"
:src="inbox.avatar_url"
:username="inbox.name"
size="48px"
/>
<img
<div
v-else
class="woot-thumbnail"
src="~dashboard/assets/images/flag.svg"
alt="No Page Image"
/>
</td>
<!-- Short Code -->
<td>
<span class="agent-name">{{ item.name }}</span>
<span v-if="item.channel_type === 'Channel::FacebookPage'">
{{ 'Facebook' }}
</span>
<span v-if="item.channel_type === 'Channel::WebWidget'">
{{ 'Website' }}
</span>
<span v-if="item.channel_type === 'Channel::TwitterProfile'">
{{ 'Twitter' }}
</span>
<span v-if="item.channel_type === 'Channel::TwilioSms'">
{{ twilioChannelName(item) }}
</span>
<span v-if="item.channel_type === 'Channel::Whatsapp'">
{{ 'Whatsapp' }}
</span>
<span v-if="item.channel_type === 'Channel::Sms'">
{{ 'Sms' }}
</span>
<span v-if="item.channel_type === 'Channel::Email'">
{{ 'Email' }}
</span>
<span v-if="item.channel_type === 'Channel::Telegram'">
{{ 'Telegram' }}
</span>
<span v-if="item.channel_type === 'Channel::Line'">
{{ 'Line' }}
</span>
<span v-if="item.channel_type === 'Channel::Api'">
{{ globalConfig.apiChannelName || 'API' }}
</span>
</td>
<!-- Action Buttons -->
<td>
<div class="button-wrapper">
<router-link
:to="accountScopedUrl(`settings/inboxes/${item.id}`)"
class="w-12 h-12 bg-black-50 dark:bg-black-800 rounded-full p-2 ring ring-opacity-20 dark:ring-opacity-80 ring-black-100 dark:ring-black-900 border border-slate-100 dark:border-slate-700/50 shadow-sm block"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
class="opacity-80 p-1"
>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
variant="smooth"
size="tiny"
icon="settings"
color-scheme="secondary"
class-names="grey-btn"
<path
fill="currentColor"
d="M1 12c0-5.185 0-7.778 1.61-9.39C4.223 1 6.816 1 12 1s7.778 0 9.39 1.61C23 4.223 23 6.816 23 12s0 7.778-1.61 9.39C19.777 23 17.184 23 12 23s-7.778 0-9.39-1.61C1 19.777 1 17.184 1 12"
opacity=".35"
/>
</router-link>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
variant="smooth"
color-scheme="alert"
size="tiny"
class-names="grey-btn"
:is-loading="loading[item.id]"
icon="dismiss-circle"
@click="openDelete(item)"
<path
fill="currentColor"
d="M2.61 21.389c1.612 1.61 4.205 1.61 9.39 1.61s7.778 0 9.39-1.61c1.492-1.493 1.601-3.829 1.61-8.29h-3.476c-.996 0-1.494 0-1.931.202c-.438.201-.762.58-1.41 1.335l-.666.777c-.648.756-.972 1.134-1.41 1.335s-.935.202-1.93.202h-.353c-.996 0-1.494 0-1.931-.202c-.438-.2-.762-.579-1.41-1.335l-.666-.777c-.648-.756-.972-1.134-1.41-1.335s-.935-.201-1.93-.201H1c.008 4.46.118 6.796 1.61 8.289"
/>
</svg>
</div>
<div>
<span class="block font-medium capitalize">
{{ inbox.name }}
</span>
<ChannelName
:channel-type="inbox.channel_type"
:medium="inbox.medium"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
<div class="hidden w-1/3 lg:block">
<span
v-dompurify-html="
useInstallationName(
$t('INBOX_MGMT.SIDEBAR_TXT'),
globalConfig.installationName
)
"
/>
</div>
</div>
<Settings
v-if="showSettings"
:show.sync="showSettings"
:on-close="closeSettings"
:inbox="selectedInbox"
/>
<td class="py-4">
<div class="flex gap-1 justify-end">
<router-link
:to="{
name: 'settings_inbox_show',
params: { inboxId: inbox.id },
}"
>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.SETTINGS')"
variant="smooth"
size="tiny"
icon="settings"
color-scheme="secondary"
class-names="grey-btn"
/>
</router-link>
<woot-button
v-if="isAdmin"
v-tooltip.top="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
variant="smooth"
color-scheme="alert"
size="tiny"
class-names="grey-btn"
icon="dismiss-circle"
@click="openDelete(inbox)"
/>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<woot-confirm-delete-modal
v-if="showDeletePopup"
@@ -227,5 +185,5 @@ export default {
@onConfirm="confirmDeletion"
@onClose="closeDelete"
/>
</div>
</SettingsLayout>
</template>

View File

@@ -0,0 +1,55 @@
<script setup>
import { useI18n } from 'dashboard/composables/useI18n';
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
const props = defineProps({
channelType: {
type: String,
required: true,
},
medium: {
type: String,
default: '',
},
});
const getters = useStoreGetters();
const { t } = useI18n();
const globalConfig = computed(() => getters['globalConfig/get'].value);
const i18nMap = {
'Channel::FacebookPage': 'MESSENGER',
'Channel::WebWidget': 'WEB_WIDGET',
'Channel::TwitterProfile': 'TWITTER_PROFILE',
'Channel::TwilioSms': 'TWILIO_SMS',
'Channel::Whatsapp': 'WHATSAPP',
'Channel::Sms': 'SMS',
'Channel::Email': 'EMAIL',
'Channel::Telegram': 'TELEGRAM',
'Channel::Line': 'LINE',
'Channel::Api': 'API',
};
const twilioChannelName = () => {
if (props.medium === 'whatsapp') {
return t(`INBOX_MGMT.CHANNELS.WHATSAPP`);
}
return t(`INBOX_MGMT.CHANNELS.TWILIO_SMS`);
};
const readableChannelName = computed(() => {
if (props.channelType === 'Channel::Api') {
return globalConfig.value.apiChannelName || t('INBOX_MGMT.CHANNELS.API');
}
if (props.channelType === 'Channel::TwilioSms') {
return twilioChannelName();
}
return t(`INBOX_MGMT.CHANNELS.${i18nMap[props.channelType]}`);
});
</script>
<template>
<span>
{{ readableChannelName }}
</span>
</template>

View File

@@ -1,8 +1,8 @@
/* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../../helper/URLHelper';
import channelFactory from './channel-factory';
const SettingsContent = () => import('../Wrapper.vue');
const SettingWrapper = () => import('../SettingsWrapper.vue');
const InboxHome = () => import('./Index.vue');
const Settings = () => import('./Settings.vue');
const InboxChannel = () => import('./InboxChannels.vue');
@@ -12,6 +12,24 @@ const FinishSetup = () => import('./FinishSetup.vue');
export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/inboxes'),
component: SettingWrapper,
children: [
{
path: '',
redirect: 'list',
},
{
path: 'list',
name: 'settings_inbox_list',
component: InboxHome,
meta: {
permissions: ['administrator'],
},
},
],
},
{
path: frontendURL('accounts/:accountId/settings/inboxes'),
component: SettingsContent,
@@ -26,18 +44,6 @@ export default {
};
},
children: [
{
path: '',
redirect: 'list',
},
{
path: 'list',
name: 'settings_inbox_list',
component: InboxHome,
meta: {
permissions: ['administrator'],
},
},
{
path: 'new',
component: InboxChannel,