chore: Update the metric card component to support generic cases (#9030)
Rename the CSAT metric card to a generic name, updated the implementation to use composition API and removed all the custom CSS in the component to conform with TailwindCSS styles --------- Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -37,15 +37,6 @@ import format from 'date-fns/format';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import ChartStats from './components/ChartElements/ChartStats.vue';
|
||||
const REPORTS_KEYS = {
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||
OUTGOING_MESSAGES: 'outgoing_messages_count',
|
||||
FIRST_RESPONSE_TIME: 'avg_first_response_time',
|
||||
RESOLUTION_TIME: 'avg_resolution_time',
|
||||
RESOLUTION_COUNT: 'resolutions_count',
|
||||
REPLY_TIME: 'reply_time',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { ChartStats },
|
||||
@@ -55,18 +46,22 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
reportKeys: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
CONVERSATIONS: 'conversations_count',
|
||||
INCOMING_MESSAGES: 'incoming_messages_count',
|
||||
OUTGOING_MESSAGES: 'outgoing_messages_count',
|
||||
FIRST_RESPONSE_TIME: 'avg_first_response_time',
|
||||
RESOLUTION_TIME: 'avg_resolution_time',
|
||||
RESOLUTION_COUNT: 'resolutions_count',
|
||||
REPLY_TIME: 'reply_time',
|
||||
}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
metrics() {
|
||||
const reportKeys = [
|
||||
'CONVERSATIONS',
|
||||
'FIRST_RESPONSE_TIME',
|
||||
'REPLY_TIME',
|
||||
'RESOLUTION_TIME',
|
||||
'RESOLUTION_COUNT',
|
||||
'INCOMING_MESSAGES',
|
||||
'OUTGOING_MESSAGES',
|
||||
];
|
||||
const reportKeys = Object.keys(this.reportKeys);
|
||||
const infoText = {
|
||||
FIRST_RESPONSE_TIME: this.$t(
|
||||
`REPORT.METRICS.FIRST_RESPONSE_TIME.INFO_TEXT`
|
||||
@@ -75,11 +70,11 @@ export default {
|
||||
};
|
||||
return reportKeys.map(key => ({
|
||||
NAME: this.$t(`REPORT.METRICS.${key}.NAME`),
|
||||
KEY: REPORTS_KEYS[key],
|
||||
KEY: this.reportKeys[key],
|
||||
DESC: this.$t(`REPORT.METRICS.${key}.DESC`),
|
||||
INFO_TEXT: infoText[key],
|
||||
TOOLTIP_TEXT: `REPORT.METRICS.${key}.TOOLTIP_TEXT`,
|
||||
trend: this.calculateTrend(REPORTS_KEYS[key]),
|
||||
trend: this.calculateTrend(this.reportKeys[key]),
|
||||
}));
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="md:w-[16%] sm:w-[50%] sm:max-w-[50%] md:max-w-[16%] csat--metric-card"
|
||||
:class="{
|
||||
disabled: disabled,
|
||||
}"
|
||||
>
|
||||
<h3 class="heading">
|
||||
<span>{{ label }}</span>
|
||||
<fluent-icon
|
||||
v-tooltip="infoText"
|
||||
size="14"
|
||||
icon="info"
|
||||
class="csat--icon"
|
||||
/>
|
||||
</h3>
|
||||
<h4 class="metric">
|
||||
{{ value }}
|
||||
</h4>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
infoText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.csat--metric-card {
|
||||
margin: 0;
|
||||
padding: var(--space-normal);
|
||||
|
||||
&.disabled {
|
||||
// grayscale everything
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.heading {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: 0;
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-bigger);
|
||||
font-weight: var(--font-weight-feather);
|
||||
margin-bottom: 0;
|
||||
margin-top: var(--space-smaller);
|
||||
@apply text-slate-700 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
.csat--icon {
|
||||
@apply text-slate-500 dark:text-slate-200 my-0 mx-0.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-wrap mx-0 bg-white dark:bg-slate-800 rounded-[4px] p-4 mb-5 border border-solid border-slate-75 dark:border-slate-700"
|
||||
class="flex-col lg:flex-row flex flex-wrap mx-0 bg-white dark:bg-slate-800 rounded-[4px] p-4 mb-5 border border-solid border-slate-75 dark:border-slate-700"
|
||||
>
|
||||
<csat-metric-card
|
||||
:label="$t('CSAT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL')"
|
||||
@@ -18,16 +18,19 @@
|
||||
:info-text="$t('CSAT_REPORTS.METRIC.RESPONSE_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(responseRate)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="metrics.totalResponseCount && !ratingFilterEnabled"
|
||||
class="w-[50%] max-w-[50%] flex-[50%] report-card rtl:[direction:initial]"
|
||||
class="w-full md:w-1/2 md:max-w-[50%] flex-1 rtl:[direction:initial] p-4"
|
||||
>
|
||||
<h3 class="heading text-slate-800 dark:text-slate-100">
|
||||
<h3
|
||||
class="flex items-center text-xs md:text-sm font-medium m-0 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<div class="flex justify-end flex-row-reverse">
|
||||
<div
|
||||
v-for="(rating, key, index) in ratingPercentage"
|
||||
:key="rating + key + index"
|
||||
class="pl-4"
|
||||
class="pr-4 rtl:pl-4"
|
||||
>
|
||||
<span class="my-0 mx-0.5">{{ ratingToEmoji(key) }}</span>
|
||||
<span>{{ formatToPercent(rating) }}</span>
|
||||
@@ -42,7 +45,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import CsatMetricCard from './CsatMetricCard.vue';
|
||||
import CsatMetricCard from './ReportMetricCard.vue';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Vue from 'vue';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import CsatMetricCard from './CsatMetricCard';
|
||||
import ReportMetricCard from './ReportMetricCard';
|
||||
|
||||
Vue.use(VTooltip, { defaultHtml: false });
|
||||
|
||||
export default {
|
||||
title: 'Components/CSAT/Metrics Card',
|
||||
component: CsatMetricCard,
|
||||
component: ReportMetricCard,
|
||||
argTypes: {
|
||||
label: {
|
||||
defaultValue: '',
|
||||
@@ -31,12 +31,12 @@ export default {
|
||||
|
||||
const Template = (_, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { CsatMetricCard },
|
||||
template: '<csat-metric-card v-bind="$props" />',
|
||||
components: { ReportMetricCard },
|
||||
template: '<report-metric-card v-bind="$props" />',
|
||||
});
|
||||
|
||||
export const CsatMetricCardTemplate = Template.bind({});
|
||||
CsatMetricCardTemplate.args = {
|
||||
export const ReportMetricCardTemplate = Template.bind({});
|
||||
ReportMetricCardTemplate.args = {
|
||||
infoText: 'No. of responses / No. of survey messages sent * 100',
|
||||
label: 'Satisfaction Score',
|
||||
value: '98.5',
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
infoText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
ref="reportMetricContainer"
|
||||
class="xs:w-full sm:max-w-[50%] lg:w-1/6 lg:max-w-[16%] m-0 p-4"
|
||||
:class="{
|
||||
'grayscale pointer-events-none opacity-30': disabled,
|
||||
}"
|
||||
>
|
||||
<h3
|
||||
class="flex items-center text-sm font-medium m-0 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<span ref="reportMetricLabel">{{ label }}</span>
|
||||
<fluent-icon
|
||||
ref="reportMetricInfo"
|
||||
v-tooltip="infoText"
|
||||
size="14"
|
||||
icon="info"
|
||||
class="text-slate-500 dark:text-slate-200 my-0 mx-1 mt-0.5"
|
||||
/>
|
||||
</h3>
|
||||
<h4
|
||||
ref="reportMetricValue"
|
||||
class="text-slate-700 dark:text-slate-100 mb-0 mt-1 font-thin text-3xl"
|
||||
>
|
||||
{{ value }}
|
||||
</h4>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,46 +1,50 @@
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import CsatMetricCard from '../CsatMetricCard.vue';
|
||||
import ReportMetricCard from '../ReportMetricCard.vue';
|
||||
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VTooltip);
|
||||
|
||||
describe('CsatMetricCard.vue', () => {
|
||||
describe('ReportMetricCard.vue', () => {
|
||||
it('renders props correctly', () => {
|
||||
const label = 'Total Responses';
|
||||
const value = '100';
|
||||
const infoText = 'Total number of responses';
|
||||
const wrapper = shallowMount(CsatMetricCard, {
|
||||
const wrapper = shallowMount(ReportMetricCard, {
|
||||
propsData: { label, value, infoText },
|
||||
localVue,
|
||||
stubs: ['fluent-icon'],
|
||||
});
|
||||
|
||||
expect(wrapper.find('.heading span').text()).toMatch(label);
|
||||
expect(wrapper.find('.metric').text()).toMatch(value);
|
||||
expect(wrapper.find('.csat--icon').classes()).toContain('has-tooltip');
|
||||
expect(wrapper.find({ ref: 'reportMetricLabel' }).text()).toMatch(label);
|
||||
expect(wrapper.find({ ref: 'reportMetricValue' }).text()).toMatch(value);
|
||||
expect(wrapper.find({ ref: 'reportMetricInfo' }).classes()).toContain(
|
||||
'has-tooltip'
|
||||
);
|
||||
});
|
||||
|
||||
it('adds disabled class when disabled prop is true', () => {
|
||||
const wrapper = shallowMount(CsatMetricCard, {
|
||||
const wrapper = shallowMount(ReportMetricCard, {
|
||||
propsData: { label: '', value: '', infoText: '', disabled: true },
|
||||
localVue,
|
||||
stubs: ['fluent-icon'],
|
||||
});
|
||||
|
||||
expect(wrapper.find('.csat--metric-card').classes()).toContain('disabled');
|
||||
expect(wrapper.classes().join(' ')).toContain(
|
||||
'grayscale pointer-events-none opacity-30'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not add disabled class when disabled prop is false', () => {
|
||||
const wrapper = shallowMount(CsatMetricCard, {
|
||||
const wrapper = shallowMount(ReportMetricCard, {
|
||||
propsData: { label: '', value: '', infoText: '', disabled: false },
|
||||
localVue,
|
||||
stubs: ['fluent-icon'],
|
||||
});
|
||||
|
||||
expect(wrapper.find('.csat--metric-card').classes()).not.toContain(
|
||||
'disabled'
|
||||
);
|
||||
expect(
|
||||
wrapper.find({ ref: 'reportMetricContainer' }).classes().join(' ')
|
||||
).not.toContain('grayscale pointer-events-none opacity-30');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user