mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
fix: stale report value shown if summary fetch breaks (#11270)
This pull request includes several changes to better show the report metrics fetching status on the dashboard. This also prevents showing stale data in case fetching summary fails due to timeout or other issue The most important changes include adding a new fetching status state to the store called `summaryFetchingStatus`, updating the `useReportMetrics` composable, and modifying the `ChartStats` component to handle different fetching statuses. #### Loading  #### Finished  #### Failed <img width="1512" alt="image" src="https://github.com/user-attachments/assets/d521a785-9299-4e59-94dc-561a7a84377e" /> --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -7,8 +7,12 @@ import { formatTime } from '@chatwoot/utils';
|
|||||||
* @param {string} [accountSummaryKey='getAccountSummary'] - The key for accessing account summary data.
|
* @param {string} [accountSummaryKey='getAccountSummary'] - The key for accessing account summary data.
|
||||||
* @returns {Object} An object containing utility functions for report metrics.
|
* @returns {Object} An object containing utility functions for report metrics.
|
||||||
*/
|
*/
|
||||||
export function useReportMetrics(accountSummaryKey = 'getAccountSummary') {
|
export function useReportMetrics(
|
||||||
|
accountSummaryKey = 'getAccountSummary',
|
||||||
|
summarFetchingKey = 'getAccountSummaryFetchingStatus'
|
||||||
|
) {
|
||||||
const accountSummary = useMapGetter(accountSummaryKey);
|
const accountSummary = useMapGetter(accountSummaryKey);
|
||||||
|
const fetchingStatus = useMapGetter(summarFetchingKey);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the trend percentage for a given metric.
|
* Calculates the trend percentage for a given metric.
|
||||||
@@ -53,5 +57,6 @@ export function useReportMetrics(accountSummaryKey = 'getAccountSummary') {
|
|||||||
calculateTrend,
|
calculateTrend,
|
||||||
isAverageMetricType,
|
isAverageMetricType,
|
||||||
displayMetric,
|
displayMetric,
|
||||||
|
fetchingStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default {
|
|||||||
<BotMetrics :filters="requestPayload" />
|
<BotMetrics :filters="requestPayload" />
|
||||||
<ReportContainer
|
<ReportContainer
|
||||||
account-summary-key="getBotSummary"
|
account-summary-key="getBotSummary"
|
||||||
|
summary-fetching-key="getBotSummaryFetchingStatus"
|
||||||
:group-by="groupBy"
|
:group-by="groupBy"
|
||||||
:report-keys="reportKeys"
|
:report-keys="reportKeys"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'getAccountSummary',
|
default: 'getAccountSummary',
|
||||||
},
|
},
|
||||||
|
summaryFetchingKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'getAccountSummaryFetchingStatus',
|
||||||
|
},
|
||||||
reportKeys: {
|
reportKeys: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
@@ -148,7 +152,11 @@ export default {
|
|||||||
:key="metric.KEY"
|
:key="metric.KEY"
|
||||||
class="p-4 mb-3 rounded-md"
|
class="p-4 mb-3 rounded-md"
|
||||||
>
|
>
|
||||||
<ChartStats :metric="metric" :account-summary-key="accountSummaryKey" />
|
<ChartStats
|
||||||
|
:metric="metric"
|
||||||
|
:account-summary-key="accountSummaryKey"
|
||||||
|
:summary-fetching-key="summaryFetchingKey"
|
||||||
|
/>
|
||||||
<div class="mt-4 h-72">
|
<div class="mt-4 h-72">
|
||||||
<woot-loading-state
|
<woot-loading-state
|
||||||
v-if="accountReport.isFetching[metric.KEY]"
|
v-if="accountReport.isFetching[metric.KEY]"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useReportMetrics } from 'dashboard/composables/useReportMetrics';
|
import { useReportMetrics } from 'dashboard/composables/useReportMetrics';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
import { STATUS } from 'dashboard/store/constants';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
metric: {
|
metric: {
|
||||||
@@ -10,11 +13,16 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'getAccountSummary',
|
default: 'getAccountSummary',
|
||||||
},
|
},
|
||||||
|
summaryFetchingKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'getAccountSummaryFetchingStatus',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { calculateTrend, displayMetric, isAverageMetricType } = useReportMetrics(
|
const { t } = useI18n();
|
||||||
props.accountSummaryKey
|
|
||||||
);
|
const { calculateTrend, displayMetric, isAverageMetricType, fetchingStatus } =
|
||||||
|
useReportMetrics(props.accountSummaryKey, props.summaryFetchingKey);
|
||||||
|
|
||||||
const trendColor = (value, key) => {
|
const trendColor = (value, key) => {
|
||||||
if (isAverageMetricType(key)) {
|
if (isAverageMetricType(key)) {
|
||||||
@@ -34,10 +42,25 @@ const trendColor = (value, key) => {
|
|||||||
{{ metric.NAME }}
|
{{ metric.NAME }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-end text-n-slate-12">
|
<div class="flex items-end text-n-slate-12">
|
||||||
<div class="text-xl font-medium">
|
<div v-if="fetchingStatus === STATUS.FETCHING">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="fetchingStatus === STATUS.FAILED"
|
||||||
|
class="text-n-ruby-10 text-sm"
|
||||||
|
>
|
||||||
|
{{ t('REPORT.SUMMARY_FETCHING_FAILED') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="fetchingStatus === STATUS.FINISHED"
|
||||||
|
class="text-xl font-medium"
|
||||||
|
>
|
||||||
{{ displayMetric(metric.KEY) }}
|
{{ displayMetric(metric.KEY) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="metric.trend" class="text-xs ml-4 flex items-center mb-0.5">
|
<div
|
||||||
|
v-if="metric.trend && fetchingStatus === STATUS.FINISHED"
|
||||||
|
class="text-xs ml-4 flex items-center mb-0.5"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="metric.trend < 0"
|
v-if="metric.trend < 0"
|
||||||
class="h-0 w-0 border-x-4 medium border-x-transparent border-t-[8px] mr-1"
|
class="h-0 w-0 border-x-4 medium border-x-transparent border-t-[8px] mr-1"
|
||||||
|
|||||||
5
app/javascript/dashboard/store/constants.js
Normal file
5
app/javascript/dashboard/store/constants.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const STATUS = {
|
||||||
|
FAILED: 'failed',
|
||||||
|
FETCHING: 'fetching',
|
||||||
|
FINISHED: 'finished',
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint no-console: 0 */
|
/* eslint no-console: 0 */
|
||||||
import * as types from '../mutation-types';
|
import * as types from '../mutation-types';
|
||||||
|
import { STATUS } from '../constants';
|
||||||
import Report from '../../api/reports';
|
import Report from '../../api/reports';
|
||||||
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
|
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
|
||||||
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
||||||
@@ -9,6 +10,8 @@ import liveReports from '../../api/liveReports';
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
fetchingStatus: false,
|
fetchingStatus: false,
|
||||||
|
accountSummaryFetchingStatus: STATUS.FINISHED,
|
||||||
|
botSummaryFetchingStatus: STATUS.FINISHED,
|
||||||
accountReport: {
|
accountReport: {
|
||||||
isFetching: {
|
isFetching: {
|
||||||
conversations_count: false,
|
conversations_count: false,
|
||||||
@@ -74,6 +77,12 @@ const getters = {
|
|||||||
getBotSummary(_state) {
|
getBotSummary(_state) {
|
||||||
return _state.botSummary;
|
return _state.botSummary;
|
||||||
},
|
},
|
||||||
|
getAccountSummaryFetchingStatus(_state) {
|
||||||
|
return _state.accountSummaryFetchingStatus;
|
||||||
|
},
|
||||||
|
getBotSummaryFetchingStatus(_state) {
|
||||||
|
return _state.botSummaryFetchingStatus;
|
||||||
|
},
|
||||||
getAccountConversationMetric(_state) {
|
getAccountConversationMetric(_state) {
|
||||||
return _state.overview.accountConversationMetric;
|
return _state.overview.accountConversationMetric;
|
||||||
},
|
},
|
||||||
@@ -122,6 +131,7 @@ export const actions = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchAccountSummary({ commit }, reportObj) {
|
fetchAccountSummary({ commit }, reportObj) {
|
||||||
|
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FETCHING);
|
||||||
Report.getSummary(
|
Report.getSummary(
|
||||||
reportObj.from,
|
reportObj.from,
|
||||||
reportObj.to,
|
reportObj.to,
|
||||||
@@ -132,12 +142,14 @@ export const actions = {
|
|||||||
)
|
)
|
||||||
.then(accountSummary => {
|
.then(accountSummary => {
|
||||||
commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data);
|
commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data);
|
||||||
|
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FINISHED);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FAILED);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchBotSummary({ commit }, reportObj) {
|
fetchBotSummary({ commit }, reportObj) {
|
||||||
|
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FETCHING);
|
||||||
Report.getBotSummary({
|
Report.getBotSummary({
|
||||||
from: reportObj.from,
|
from: reportObj.from,
|
||||||
to: reportObj.to,
|
to: reportObj.to,
|
||||||
@@ -146,9 +158,10 @@ export const actions = {
|
|||||||
})
|
})
|
||||||
.then(botSummary => {
|
.then(botSummary => {
|
||||||
commit(types.default.SET_BOT_SUMMARY, botSummary.data);
|
commit(types.default.SET_BOT_SUMMARY, botSummary.data);
|
||||||
|
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FINISHED);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FAILED);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchAccountConversationMetric({ commit }, params = {}) {
|
fetchAccountConversationMetric({ commit }, params = {}) {
|
||||||
@@ -277,6 +290,12 @@ const mutations = {
|
|||||||
[types.default.TOGGLE_ACCOUNT_REPORT_LOADING](_state, { metric, value }) {
|
[types.default.TOGGLE_ACCOUNT_REPORT_LOADING](_state, { metric, value }) {
|
||||||
_state.accountReport.isFetching[metric] = value;
|
_state.accountReport.isFetching[metric] = value;
|
||||||
},
|
},
|
||||||
|
[types.default.SET_BOT_SUMMARY_STATUS](_state, status) {
|
||||||
|
_state.botSummaryFetchingStatus = status;
|
||||||
|
},
|
||||||
|
[types.default.SET_ACCOUNT_SUMMARY_STATUS](_state, status) {
|
||||||
|
_state.accountSummaryFetchingStatus = status;
|
||||||
|
},
|
||||||
[types.default.TOGGLE_HEATMAP_LOADING](_state, flag) {
|
[types.default.TOGGLE_HEATMAP_LOADING](_state, flag) {
|
||||||
_state.overview.uiFlags.isFetchingAccountConversationsHeatmap = flag;
|
_state.overview.uiFlags.isFetchingAccountConversationsHeatmap = flag;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,122 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { actions } from '../../reports';
|
import { actions } from '../../reports';
|
||||||
|
import * as types from '../../../mutation-types';
|
||||||
|
import { STATUS } from '../../../constants';
|
||||||
import * as DownloadHelper from 'dashboard/helper/downloadHelper';
|
import * as DownloadHelper from 'dashboard/helper/downloadHelper';
|
||||||
|
import { flushPromises } from '@vue/test-utils';
|
||||||
|
|
||||||
global.open = vi.fn();
|
global.open = vi.fn();
|
||||||
global.axios = axios;
|
global.axios = axios;
|
||||||
|
global.URL.createObjectURL = vi.fn();
|
||||||
|
|
||||||
vi.mock('axios');
|
vi.mock('axios');
|
||||||
vi.spyOn(DownloadHelper, 'downloadCsvFile');
|
vi.spyOn(DownloadHelper, 'downloadCsvFile');
|
||||||
|
|
||||||
describe('#actions', () => {
|
describe('#actions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#fetchAccountSummary', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
const commit = vi.fn();
|
||||||
|
const reportObj = {
|
||||||
|
from: 1630504922510,
|
||||||
|
to: 1630504922510,
|
||||||
|
type: 'account',
|
||||||
|
id: 1,
|
||||||
|
groupBy: 'day',
|
||||||
|
businessHours: true,
|
||||||
|
};
|
||||||
|
const summaryData = {
|
||||||
|
conversations_count: 10,
|
||||||
|
incoming_messages_count: 20,
|
||||||
|
outgoing_messages_count: 15,
|
||||||
|
avg_first_response_time: 30,
|
||||||
|
avg_resolution_time: 60,
|
||||||
|
resolutions_count: 5,
|
||||||
|
bot_resolutions_count: 2,
|
||||||
|
bot_handoffs_count: 1,
|
||||||
|
reply_time: 25,
|
||||||
|
};
|
||||||
|
axios.get.mockResolvedValue({ data: summaryData });
|
||||||
|
|
||||||
|
actions.fetchAccountSummary({ commit }, reportObj);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||||
|
[types.default.SET_ACCOUNT_SUMMARY, summaryData],
|
||||||
|
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FINISHED],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends correct actions if API fails', async () => {
|
||||||
|
const commit = vi.fn();
|
||||||
|
const reportObj = {
|
||||||
|
from: 1630504922510,
|
||||||
|
to: 1630504922510,
|
||||||
|
};
|
||||||
|
axios.get.mockRejectedValue(new Error('API Error'));
|
||||||
|
|
||||||
|
actions.fetchAccountSummary({ commit }, reportObj);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||||
|
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FAILED],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#fetchBotSummary', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
const commit = vi.fn();
|
||||||
|
const reportObj = {
|
||||||
|
from: 1630504922510,
|
||||||
|
to: 1630504922510,
|
||||||
|
groupBy: 'day',
|
||||||
|
businessHours: true,
|
||||||
|
};
|
||||||
|
const summaryData = {
|
||||||
|
bot_resolutions_count: 10,
|
||||||
|
bot_handoffs_count: 5,
|
||||||
|
previous: {
|
||||||
|
bot_resolutions_count: 8,
|
||||||
|
bot_handoffs_count: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
axios.get.mockResolvedValue({ data: summaryData });
|
||||||
|
|
||||||
|
actions.fetchBotSummary({ commit }, reportObj);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||||
|
[types.default.SET_BOT_SUMMARY, summaryData],
|
||||||
|
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FINISHED],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends correct actions if API fails', async () => {
|
||||||
|
const commit = vi.fn();
|
||||||
|
const reportObj = {
|
||||||
|
from: 1630504922510,
|
||||||
|
to: 1630504922510,
|
||||||
|
};
|
||||||
|
const error = new Error('API error');
|
||||||
|
axios.get.mockRejectedValueOnce(error);
|
||||||
|
|
||||||
|
actions.fetchBotSummary({ commit }, reportObj);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||||
|
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FAILED],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#downloadAgentReports', () => {
|
describe('#downloadAgentReports', () => {
|
||||||
it('open CSV download prompt if API is success', async () => {
|
it('open CSV download prompt if API is success', async () => {
|
||||||
const data = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
|
const data = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
|
||||||
@@ -20,7 +128,9 @@ describe('#actions', () => {
|
|||||||
to: 1630504922510,
|
to: 1630504922510,
|
||||||
fileName: 'agent-report-01-09-2021.csv',
|
fileName: 'agent-report-01-09-2021.csv',
|
||||||
};
|
};
|
||||||
await actions.downloadAgentReports(1, param);
|
actions.downloadAgentReports(1, param);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||||
param.fileName,
|
param.fileName,
|
||||||
data
|
data
|
||||||
@@ -39,7 +149,9 @@ describe('#actions', () => {
|
|||||||
type: 'label',
|
type: 'label',
|
||||||
fileName: 'label-report-01-09-2021.csv',
|
fileName: 'label-report-01-09-2021.csv',
|
||||||
};
|
};
|
||||||
await actions.downloadLabelReports(1, param);
|
actions.downloadLabelReports(1, param);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||||
param.fileName,
|
param.fileName,
|
||||||
data
|
data
|
||||||
@@ -59,7 +171,9 @@ describe('#actions', () => {
|
|||||||
to: 1635013800,
|
to: 1635013800,
|
||||||
fileName: 'inbox-report-24-10-2021.csv',
|
fileName: 'inbox-report-24-10-2021.csv',
|
||||||
};
|
};
|
||||||
await actions.downloadInboxReports(1, param);
|
actions.downloadInboxReports(1, param);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||||
param.fileName,
|
param.fileName,
|
||||||
data
|
data
|
||||||
@@ -78,7 +192,9 @@ describe('#actions', () => {
|
|||||||
to: 1635013800,
|
to: 1635013800,
|
||||||
fileName: 'inbox-report-24-10-2021.csv',
|
fileName: 'inbox-report-24-10-2021.csv',
|
||||||
};
|
};
|
||||||
await actions.downloadInboxReports(1, param);
|
actions.downloadInboxReports(1, param);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||||
param.fileName,
|
param.fileName,
|
||||||
data
|
data
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ export default {
|
|||||||
SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY',
|
SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY',
|
||||||
SET_BOT_SUMMARY: 'SET_BOT_SUMMARY',
|
SET_BOT_SUMMARY: 'SET_BOT_SUMMARY',
|
||||||
TOGGLE_ACCOUNT_REPORT_LOADING: 'TOGGLE_ACCOUNT_REPORT_LOADING',
|
TOGGLE_ACCOUNT_REPORT_LOADING: 'TOGGLE_ACCOUNT_REPORT_LOADING',
|
||||||
|
SET_BOT_SUMMARY_STATUS: 'SET_BOT_SUMMARY_STATUS',
|
||||||
|
SET_ACCOUNT_SUMMARY_STATUS: 'SET_ACCOUNT_SUMMARY_STATUS',
|
||||||
SET_ACCOUNT_CONVERSATION_METRIC: 'SET_ACCOUNT_CONVERSATION_METRIC',
|
SET_ACCOUNT_CONVERSATION_METRIC: 'SET_ACCOUNT_CONVERSATION_METRIC',
|
||||||
TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING:
|
TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING:
|
||||||
'TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING',
|
'TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING',
|
||||||
|
|||||||
Reference in New Issue
Block a user