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:
Sojan Jose
2024-02-28 07:21:06 +05:30
committed by GitHub
parent 41e269e873
commit ac249c75c4
6 changed files with 93 additions and 120 deletions

View File

@@ -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]),
}));
},
},

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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>

View File

@@ -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');
});
});