feat: Update the design for dashboard_apps (#9840)

This PR migrates the dashboard apps page to the new layout and includes
the following updates:

- Create a compact design for the back button
- Add a back button to the settings header
- Reduce letter-spacing on the description
- Fix mobile styles
- Migrate the layout of dashboard apps/index to new layouts


Note: I've moved all feature help URLs from features.yml to the frontend. This change prevents features.yml from becoming bloated due to frontend modifications.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Pranav
2024-07-25 16:26:00 -07:00
committed by GitHub
parent 0331815cc5
commit 6694db093f
11 changed files with 188 additions and 161 deletions

View File

@@ -1,6 +1,10 @@
table {
@apply border-spacing-0 text-sm w-full;
}
.woot-table {
thead {
th {
@apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200;
@@ -16,9 +20,7 @@ table {
@apply p-2.5 text-slate-700 dark:text-slate-100;
}
}
}
.woot-table {
tr {
.show-if-hover {
transition: opacity 0.2s $swift-ease-out-function;

View File

@@ -1,34 +1,40 @@
<script setup>
import router from '../../routes/index';
const props = defineProps({
backUrl: {
type: [String, Object],
default: '',
},
buttonLabel: {
type: String,
default: '',
},
compact: {
type: Boolean,
default: false,
},
});
const goBack = () => {
if (props.backUrl !== '') {
router.push(props.backUrl);
} else {
router.go(-1);
}
};
const buttonStyleClass = props.compact
? 'text-sm text-slate-600 dark:text-slate-300'
: 'text-base text-woot-500 dark:text-woot-500';
</script>
<template>
<button
class="header-section flex items-center text-base font-normal mr-4 ml-2 p-0 cursor-pointer text-woot-500 dark:text-woot-500"
class="flex items-center font-normal p-0 cursor-pointer"
:class="buttonStyleClass"
@click.capture="goBack"
>
<fluent-icon icon="chevron-left" />
<fluent-icon icon="chevron-left" class="-ml-1" />
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
</button>
</template>
<script>
import router from '../../routes/index';
export default {
props: {
backUrl: {
type: [String, Object],
default: '',
},
buttonLabel: {
type: String,
default: '',
},
},
methods: {
goBack() {
if (this.backUrl !== '') {
router.push(this.backUrl);
} else {
router.go(-1);
}
},
},
};
</script>

View File

@@ -1,4 +1,20 @@
const FEATURE_HELP_URLS = {
channel_email: 'https://chwt.app/hc/email',
channel_facebook: 'https://chwt.app/hc/fb',
help_center: 'https://chwt.app/hc/help-center',
agent_bots: 'https://chwt.app/hc/agent-bots',
team_management: 'https://chwt.app/hc/teams',
labels: 'https://chwt.app/hc/labels',
custom_attributes: 'https://chwt.app/hc/custom-attributes',
canned_responses: 'https://chwt.app/hc/canned',
integrations: 'https://chwt.app/hc/integrations',
campaigns: 'https://chwt.app/hc/campaigns',
reports: 'https://chwt.app/hc/reports',
message_reply_to: 'https://chwt.app/hc/reply-to',
sla: 'https://chwt.app/hc/sla',
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
};
export function getHelpUrlForFeature(featureName) {
const { helpUrls } = window.chatwootConfig;
return helpUrls[featureName];
return FEATURE_HELP_URLS[featureName];
}

View File

@@ -172,6 +172,7 @@
"HEADER_BTN_TXT": "Add a new dashboard app",
"SIDEBAR_TXT": "<p><b>Dashboard Apps</b></p><p>Dashboard Apps allow organizations to embed an application inside the Chatwoot dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that inside the dashboard to provide user information, their orders, or their previous payment history.</p><p>When you embed your application using the dashboard in Chatwoot, your application will get the context of the conversation and contact as a window event. Implement a listener for the message event on your page to receive the context.</p><p>To add a new dashboard app, click on the button 'Add a new dashboard app'.</p>",
"DESCRIPTION": "Dashboard Apps allow organizations to embed an application inside the dashboard to provide the context for customer support agents. This feature allows you to create an application independently and embed that to provide user information, their orders, or their previous payment history.",
"LEARN_MORE": "Learn more about Dashboard Apps",
"LIST": {
"404": "There are no dashboard apps configured on this account yet",
"LOADING": "Fetching dashboard apps...",

View File

@@ -10,6 +10,7 @@
v-if="showBackButton"
:button-label="backButtonLabel"
:back-url="backUrl"
class="ml-2 mr-4"
/>
<fluent-icon
v-if="icon"

View File

@@ -9,7 +9,7 @@ defineProps({
<template>
<div
class="flex flex-col w-full h-full m-0 px-8 lg:px-16 py-8 overflow-auto bg-white dark:bg-slate-900"
class="flex flex-col w-full h-full m-0 p-6 sm:py-8 lg:px-16 overflow-auto bg-white dark:bg-slate-900 font-inter"
>
<div class="flex items-start w-full max-w-6xl mx-auto">
<keep-alive v-if="keepAlive">

View File

@@ -1,6 +1,7 @@
<script setup>
import CustomBrandPolicyWrapper from 'dashboard/components/CustomBrandPolicyWrapper.vue';
import { getHelpUrlForFeature } from '../../../../helper/featureHelper';
import BackButton from '../../../../components/widgets/BackButton.vue';
const props = defineProps({
title: {
type: String,
@@ -22,6 +23,10 @@ const props = defineProps({
type: String,
default: '',
},
backButtonLabel: {
type: String,
default: '',
},
});
const helpURL = getHelpUrlForFeature(props.featureName);
@@ -33,7 +38,12 @@ const openInNewTab = url => {
</script>
<template>
<div class="flex flex-col items-start w-full gap-3 pt-4">
<div class="flex flex-col items-start w-full gap-2 pt-4">
<BackButton
v-if="backButtonLabel"
compact
:button-label="backButtonLabel"
/>
<div class="flex items-center justify-between w-full gap-4">
<div class="flex items-center gap-3">
<div
@@ -64,7 +74,7 @@ const openInNewTab = url => {
</div>
<div class="flex flex-col gap-3 text-slate-600 dark:text-slate-300 w-full">
<p
class="mb-0 text-base font-normal line-clamp-5 sm:line-clamp-none max-w-3xl"
class="mb-0 text-base font-normal line-clamp-5 sm:line-clamp-none max-w-3xl tracking-[-0.1px]"
>
<slot name="description">{{ description }}</slot>
</p>
@@ -74,7 +84,7 @@ const openInNewTab = url => {
:href="helpURL"
target="_blank"
rel="noopener noreferrer"
class="sm:inline-flex hidden tracking-[-0.6%] gap-1 w-fit items-center text-woot-500 dark:text-woot-500 text-sm font-medium tracking=[-0.6%] hover:underline"
class="sm:inline-flex hidden gap-1 w-fit items-center text-woot-500 dark:text-woot-500 text-sm font-medium hover:underline"
>
{{ linkText }}
<fluent-icon
@@ -86,14 +96,16 @@ const openInNewTab = url => {
</a>
</CustomBrandPolicyWrapper>
</div>
<div class="flex items-start justify-start w-full gap-3 sm:hidden">
<div
class="flex items-start justify-start w-full gap-3 sm:hidden flex-wrap"
>
<slot name="actions" />
<CustomBrandPolicyWrapper :show-on-custom-branded-instance="false">
<woot-button
v-if="helpURL && linkText"
color-scheme="secondary"
icon="arrow-outwards"
class="flex-row-reverse rounded-xl min-w-0 !bg-slate-50 !text-slate-900 dark:!text-white dark:!bg-slate-800"
class="flex-row-reverse rounded-md min-w-0 !bg-slate-50 !text-slate-900 dark:!text-white dark:!bg-slate-800"
@click="openInNewTab(helpURL)"
>
{{ linkText }}

View File

@@ -1,12 +1,26 @@
<script setup>
defineProps({
app: {
type: Object,
default: () => ({}),
},
});
defineEmits(['edit', 'delete']);
</script>
<template>
<tr>
<td class="w-40 max-w-[10rem] truncate" :title="app.title">
<tr class="py-1 max-w-full">
<td
class="py-4 pr-4 text-sm w-40 max-w-[10rem] truncate"
:title="app.title"
>
{{ app.title }}
</td>
<td class="max-w-xs truncate" :title="app.content[0].url">
<td class="py-4 pr-4 text-sm max-w-lg truncate" :title="app.content[0].url">
{{ app.content[0].url }}
</td>
<td class="flex justify-end gap-1">
<td class="py-4 pr-4 text-sm flex gap-2 sm:pr-0">
<woot-button
v-tooltip.top="
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.EDIT_TOOLTIP')
@@ -32,30 +46,3 @@
</td>
</tr>
</template>
<script>
export default {
loading: {
type: Boolean,
default: false,
},
props: {
app: {
type: Object,
default: () => ({}),
},
},
};
</script>
<style lang="scss" scoped>
.dashboard-app-label-url {
@apply relative w-full;
&:before {
@apply invisible content-['&nbsp'];
}
span {
@apply absolute left-0 right-0;
}
}
</style>

View File

@@ -1,95 +1,14 @@
<template>
<div class="flex-1 p-4 overflow-auto">
<woot-button
color-scheme="success"
class-names="button--fixed-top"
icon="add-circle"
@click="openCreatePopup"
>
{{ $t('INTEGRATION_SETTINGS.DASHBOARD_APPS.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"
>
{{ $t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.404') }}
</p>
<woot-loading-state
v-if="uiFlags.isFetching"
:message="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.LOADING')"
/>
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
<thead>
<th
v-for="thHeader in $t(
'INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.TABLE_HEADER'
)"
:key="thHeader"
>
{{ thHeader }}
</th>
</thead>
<tbody>
<dashboard-apps-row
v-for="(dashboardAppItem, index) in records"
:key="dashboardAppItem.id"
:index="index"
:app="dashboardAppItem"
@edit="editApp"
@delete="openDeletePopup"
/>
</tbody>
</table>
</div>
<div class="hidden w-1/3 lg:block">
<span
v-dompurify-html="
useInstallationName(
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.SIDEBAR_TXT'),
globalConfig.installationName
)
"
/>
</div>
</div>
<dashboard-app-modal
v-if="showDashboardAppPopup"
:show="showDashboardAppPopup"
:mode="mode"
:selected-app-data="selectedApp"
@close="toggleDashboardAppPopup"
/>
<woot-delete-modal
:show.sync="showDeleteConfirmationPopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.TITLE')"
:message="
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.MESSAGE', {
appName: selectedApp.title,
})
"
:confirm-text="
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.CONFIRM_YES')
"
:reject-text="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.CONFIRM_NO')"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import DashboardAppModal from './DashboardAppModal.vue';
import DashboardAppsRow from './DashboardAppsRow.vue';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import BaseSettingsHeader from '../../components/BaseSettingsHeader.vue';
export default {
components: {
BaseSettingsHeader,
DashboardAppModal,
DashboardAppsRow,
},
@@ -156,3 +75,87 @@ export default {
},
};
</script>
<template>
<div class="flex-1 overflow-auto flex gap-8 flex-col">
<BaseSettingsHeader
:title="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.TITLE')"
:description="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DESCRIPTION')"
:link-text="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LEARN_MORE')"
feature-name="dashboard_apps"
:back-button-label="$t('INTEGRATION_SETTINGS.HEADER')"
>
<template #actions>
<woot-button
class="button nice rounded-md"
icon="add-circle"
@click="openCreatePopup"
>
{{ $t('INTEGRATION_SETTINGS.DASHBOARD_APPS.HEADER_BTN_TXT') }}
</woot-button>
</template>
</BaseSettingsHeader>
<div class="w-full text-slate-700 dark:text-slate-200 overflow-x-auto">
<p
v-if="!uiFlags.isFetching && !records.length"
class="flex flex-col items-center justify-center h-full"
>
{{ $t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.404') }}
</p>
<woot-loading-state
v-if="uiFlags.isFetching"
:message="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.LIST.LOADING')"
/>
<table
v-if="!uiFlags.isFetching && records.length"
class="min-w-full divide-y divide-slate-75 dark:divide-slate-700"
>
<thead>
<th
v-for="thHeader in $t(
'INTEGRATION_SETTINGS.DASHBOARD_APPS.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">
<dashboard-apps-row
v-for="(dashboardAppItem, index) in records"
:key="dashboardAppItem.id"
:index="index"
:app="dashboardAppItem"
@edit="editApp"
@delete="openDeletePopup"
/>
</tbody>
</table>
</div>
<dashboard-app-modal
v-if="showDashboardAppPopup"
:show="showDashboardAppPopup"
:mode="mode"
:selected-app-data="selectedApp"
@close="toggleDashboardAppPopup"
/>
<woot-delete-modal
:show.sync="showDeleteConfirmationPopup"
:on-close="closeDeletePopup"
:on-confirm="confirmDeletion"
:title="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.TITLE')"
:message="
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.MESSAGE', {
appName: selectedApp.title,
})
"
:confirm-text="
$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.CONFIRM_YES')
"
:reject-text="$t('INTEGRATION_SETTINGS.DASHBOARD_APPS.DELETE.CONFIRM_NO')"
/>
</div>
</template>

View File

@@ -33,7 +33,7 @@ onMounted(() => {
/>
</template>
<template #body>
<div class="flex-grow flex-shrink overflow-auto font-inter">
<div class="flex-grow flex-shrink overflow-auto">
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<integration-item
v-for="item in integrationList"

View File

@@ -22,9 +22,16 @@ export default {
permissions: ['administrator'],
},
},
{
path: 'dashboard_apps',
component: DashboardApps,
name: 'settings_integrations_dashboard_apps',
meta: {
permissions: ['administrator'],
},
},
],
},
{
path: frontendURL('accounts/:accountId/settings/integrations'),
component: SettingsContent,
@@ -50,14 +57,6 @@ export default {
permissions: ['administrator'],
},
},
{
path: 'dashboard_apps',
component: DashboardApps,
name: 'settings_integrations_dashboard_apps',
meta: {
permissions: ['administrator'],
},
},
{
path: 'slack',
name: 'settings_integrations_slack',