diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index ef3a58979..6093bf6b0 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -43,9 +43,7 @@ import { import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../helper/AnalyticsHelper'; +import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; const createState = (content, placeholder, plugins = []) => { return EditorState.create({ @@ -265,7 +263,7 @@ export default { ); this.state = this.editorView.state.apply(tr); this.emitOnChange(); - AnalyticsHelper.track(ANALYTICS_EVENTS.USED_MENTIONS); + this.$track(CONVERSATION_EVENTS.USED_MENTIONS); return false; }, @@ -295,7 +293,7 @@ export default { this.emitOnChange(); tr.scrollIntoView(); - AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE); + this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); return false; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index fe25419a9..56937b3ec 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -164,9 +164,7 @@ import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage'; import { trimContent, debounce } from '@chatwoot/utils'; import wootConstants from 'dashboard/constants'; import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../helper/AnalyticsHelper'; +import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; const EmojiInput = () => import('shared/components/emoji/EmojiInput'); @@ -710,7 +708,7 @@ export default { }, replaceText(message) { setTimeout(() => { - AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE); + this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); this.message = message; }, 100); }, diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/events.js b/app/javascript/dashboard/helper/AnalyticsHelper/events.js index b0fa7ee1a..3d8b0f901 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/events.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/events.js @@ -1,9 +1,73 @@ -export const EXECUTED_A_MACRO = 'Executed a macro'; -export const SENT_MESSAGE = 'Sent a message'; -export const SENT_PRIVATE_NOTE = 'Sent a private note'; -export const INSERTED_A_CANNED_RESPONSE = 'Inserted a canned response'; -export const USED_MENTIONS = 'Used mentions'; -export const MERGED_CONTACTS = 'Used merge contact option'; -export const ADDED_TO_CANNED_RESPONSE = 'Used added to canned response option'; -export const ADDED_A_CUSTOM_ATTRIBUTE = 'Added a custom attribute'; -export const ADDED_AN_INBOX = 'Added an inbox'; +export const CONVERSATION_EVENTS = Object.freeze({ + EXECUTED_A_MACRO: 'Executed a macro', + SENT_MESSAGE: 'Sent a message', + SENT_PRIVATE_NOTE: 'Sent a private note', + INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response', + USED_MENTIONS: 'Used mentions', +}); + +export const ACCOUNT_EVENTS = Object.freeze({ + ADDED_TO_CANNED_RESPONSE: 'Used added to canned response option', + ADDED_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute', + ADDED_AN_INBOX: 'Added an inbox', +}); + +export const LABEL_EVENTS = Object.freeze({ + CREATE: 'Created a label', + UPDATE: 'Updated a label', + DELETED: 'Deleted a label', + APPLY_LABEL: 'Applied a label', +}); + +// REPORTS EVENTS +export const REPORTS_EVENTS = Object.freeze({ + DOWNLOAD_REPORT: 'Downloaded a report', + FILTER_REPORT: 'Used filters in the reports', +}); + +// CONTACTS PAGE EVENTS +export const CONTACTS_EVENTS = Object.freeze({ + APPLY_FILTER: 'Applied filters in the contacts list', + SAVE_FILTER: 'Saved a filter in the contacts list', + DELETE_FILTER: 'Deleted a filter in the contacts list', + + APPLY_SORT: 'Sorted contacts list', + SEARCH: 'Searched contacts list', + CREATE_CONTACT: 'Created a contact', + MERGED_CONTACTS: 'Used merge contact option', + IMPORT_MODAL_OPEN: 'Opened import contacts modal', + IMPORT_FAILURE: 'Import contacts failed', + IMPORT_SUCCESS: 'Imported contacts successfully', +}); + +// CAMPAIGN EVENTS +export const CAMPAIGNS_EVENTS = Object.freeze({ + OPEN_NEW_CAMPAIGN_MODAL: 'Opened new campaign modal', + CREATE_CAMPAIGN: 'Created a new campaign', + UPDATE_CAMPAIGN: 'Updated a campaign', + DELETE_CAMPAIGN: 'Deleted a campaign', +}); + +// PORTAL EVENTS +export const PORTALS_EVENTS = Object.freeze({ + ONBOARD_BASIC_INFORMATION: 'New Portal: Completed basic information', + ONBOARD_CUSTOMIZATION: 'New portal: Completed customization', + CREATE_PORTAL: 'Created a portal', + DELETE_PORTAL: 'Deleted a portal', + UPDATE_PORTAL: 'Updated a portal', + + CREATE_LOCALE: 'Created a portal locale', + SET_DEFAULT_LOCALE: 'Set default portal locale', + DELETE_LOCALE: 'Deleted a portal locale', + SWITCH_LOCALE: 'Switched portal locale', + + CREATE_CATEGORY: 'Created a portal category', + DELETE_CATEGORY: 'Deleted a portal category', + EDIT_CATEGORY: 'Edited a portal category', + + CREATE_ARTICLE: 'Created an article', + PUBLISH_ARTICLE: 'Published an article', + ARCHIVE_ARTICLE: 'Archived an article', + DELETE_ARTICLE: 'Deleted an article', + PREVIEW_ARTICLE: 'Previewed article', +}); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/index.js b/app/javascript/dashboard/helper/AnalyticsHelper/index.js index e082182cf..bf91d9a91 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/index.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/index.js @@ -1,12 +1,26 @@ import { AnalyticsBrowser } from '@june-so/analytics-next'; -class AnalyticsHelper { +/** + * AnalyticsHelper class to initialize and track user analytics + * @class AnalyticsHelper + */ +export class AnalyticsHelper { + /** + * @constructor + * @param {Object} [options={}] - options for analytics + * @param {string} [options.token] - analytics token + */ constructor({ token: analyticsToken } = {}) { this.analyticsToken = analyticsToken; this.analytics = null; this.user = {}; } + /** + * Initialize analytics + * @function + * @async + */ async init() { if (!this.analyticsToken) { return; @@ -18,6 +32,11 @@ class AnalyticsHelper { this.analytics = analytics; } + /** + * Identify the user + * @function + * @param {Object} user - User object + */ identify(user) { if (!this.analytics) { return; @@ -41,6 +60,12 @@ class AnalyticsHelper { } } + /** + * Track any event + * @function + * @param {string} eventName - event name + * @param {Object} [properties={}] - event properties + */ track(eventName, properties = {}) { if (!this.analytics) { return; @@ -53,6 +78,11 @@ class AnalyticsHelper { }); } + /** + * Track the page views + * @function + * @param {Object} params - Page view properties + */ page(params) { if (!this.analytics) { return; @@ -62,6 +92,5 @@ class AnalyticsHelper { } } -export * as ANALYTICS_EVENTS from './events'; - +// This object is shared across, the init is called in app/javascript/packs/application.js export default new AnalyticsHelper(window.analyticsConfig); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/plugin.js b/app/javascript/dashboard/helper/AnalyticsHelper/plugin.js new file mode 100644 index 000000000..5ed12f1f5 --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/plugin.js @@ -0,0 +1,11 @@ +import analyticsHelper from '.'; + +export default { + // This function is called when the Vue plugin is installed + install(Vue) { + analyticsHelper.init(); + Vue.prototype.$analytics = analyticsHelper; + // Add a shorthand function for the track method on the helper module + Vue.prototype.$track = analyticsHelper.track.bind(analyticsHelper); + }, +}; diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/specs/events.spec.js b/app/javascript/dashboard/helper/AnalyticsHelper/specs/events.spec.js new file mode 100644 index 000000000..683b1c593 --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/specs/events.spec.js @@ -0,0 +1,26 @@ +import * as AnalyticsEvents from '../events'; + +describe('Analytics Events', () => { + it('should be frozen', () => { + Object.entries(AnalyticsEvents).forEach(([, value]) => { + expect(Object.isFrozen(value)).toBe(true); + }); + }); + + it('event names should be unique across the board', () => { + const allValues = Object.values(AnalyticsEvents).reduce( + (acc, curr) => acc.concat(Object.values(curr)), + [] + ); + const uniqueValues = new Set(allValues); + expect(allValues.length).toBe(uniqueValues.size); + }); + + it('should not allow properties to be modified', () => { + Object.values(AnalyticsEvents).forEach(eventsObject => { + expect(() => { + eventsObject.NEW_PROPERTY = 'new value'; + }).toThrow(); + }); + }); +}); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js b/app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js new file mode 100644 index 000000000..ef02ac201 --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/specs/helper.spec.js @@ -0,0 +1,139 @@ +import helperObject, { AnalyticsHelper } from '../'; + +jest.mock('@june-so/analytics-next', () => ({ + AnalyticsBrowser: { + load: () => [ + { + identify: jest.fn(), + track: jest.fn(), + page: jest.fn(), + group: jest.fn(), + }, + ], + }, +})); + +describe('helperObject', () => { + it('should return an instance of AnalyticsHelper', () => { + expect(helperObject).toBeInstanceOf(AnalyticsHelper); + }); +}); + +describe('AnalyticsHelper', () => { + let analyticsHelper; + beforeEach(() => { + analyticsHelper = new AnalyticsHelper({ token: 'test_token' }); + }); + + describe('init', () => { + it('should initialize the analytics browser with the correct token', async () => { + await analyticsHelper.init(); + expect(analyticsHelper.analytics).not.toBe(null); + }); + + it('should not initialize the analytics browser if token is not provided', async () => { + analyticsHelper = new AnalyticsHelper(); + await analyticsHelper.init(); + expect(analyticsHelper.analytics).toBe(null); + }); + }); + + describe('identify', () => { + beforeEach(() => { + analyticsHelper.analytics = { identify: jest.fn(), group: jest.fn() }; + }); + + it('should call identify on analytics browser with correct arguments', () => { + analyticsHelper.identify({ + id: '123', + email: 'test@example.com', + name: 'Test User', + avatar_url: 'avatar_url', + accounts: [{ id: '1', name: 'Account 1' }], + account_id: '1', + }); + + expect(analyticsHelper.analytics.identify).toHaveBeenCalledWith( + 'test@example.com', + { + userId: '123', + email: 'test@example.com', + name: 'Test User', + avatar: 'avatar_url', + } + ); + expect(analyticsHelper.analytics.group).toHaveBeenCalled(); + }); + + it('should call identify on analytics browser without group', () => { + analyticsHelper.identify({ + id: '123', + email: 'test@example.com', + name: 'Test User', + avatar_url: 'avatar_url', + accounts: [{ id: '1', name: 'Account 1' }], + account_id: '5', + }); + + expect(analyticsHelper.analytics.group).not.toHaveBeenCalled(); + }); + + it('should not call analytics.page if analytics is null', () => { + analyticsHelper.analytics = null; + analyticsHelper.identify({}); + expect(analyticsHelper.analytics).toBe(null); + }); + }); + + describe('track', () => { + beforeEach(() => { + analyticsHelper.analytics = { track: jest.fn() }; + analyticsHelper.user = { id: '123' }; + }); + + it('should call track on analytics browser with correct arguments', () => { + analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' }); + expect(analyticsHelper.analytics.track).toHaveBeenCalledWith({ + userId: '123', + event: 'Test Event', + properties: { prop1: 'value1', prop2: 'value2' }, + }); + }); + + it('should call track on analytics browser with default properties', () => { + analyticsHelper.track('Test Event'); + expect(analyticsHelper.analytics.track).toHaveBeenCalledWith({ + userId: '123', + event: 'Test Event', + properties: {}, + }); + }); + + it('should not call track on analytics browser if analytics is not initialized', () => { + analyticsHelper.analytics = null; + analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' }); + expect(analyticsHelper.analytics).toBe(null); + }); + }); + + describe('page', () => { + beforeEach(() => { + analyticsHelper.analytics = { page: jest.fn() }; + }); + + it('should call the analytics.page method with the correct arguments', () => { + const params = { + name: 'Test page', + url: '/test', + }; + analyticsHelper.page(params); + expect(analyticsHelper.analytics.page).toHaveBeenCalledWith(params); + }); + + it('should not call analytics.page if analytics is null', () => { + analyticsHelper.analytics = null; + analyticsHelper.page(); + expect(analyticsHelper.analytics).toBe(null); + }); + }); +}); diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/specs/plugin.spec.js b/app/javascript/dashboard/helper/AnalyticsHelper/specs/plugin.spec.js new file mode 100644 index 000000000..407253f4b --- /dev/null +++ b/app/javascript/dashboard/helper/AnalyticsHelper/specs/plugin.spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import plugin from '../plugin'; +import analyticsHelper from '../index'; + +describe('Vue Analytics Plugin', () => { + beforeEach(() => { + jest.spyOn(analyticsHelper, 'init'); + jest.spyOn(analyticsHelper, 'track'); + Vue.use(plugin); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + it('should call the init method on the analyticsHelper', () => { + expect(analyticsHelper.init).toHaveBeenCalled(); + }); + + it('should add the analyticsHelper to the Vue prototype', () => { + expect(Vue.prototype.$analytics).toBe(analyticsHelper); + }); + + it('should add the track method to the Vue prototype', () => { + expect(typeof Vue.prototype.$track).toBe('function'); + Vue.prototype.$track('eventName'); + expect(analyticsHelper.track).toHaveBeenCalledWith('eventName'); + }); + + it('should call the track method on the analyticsHelper when $track is called', () => { + Vue.prototype.$track('eventName'); + expect(analyticsHelper.track).toHaveBeenCalledWith('eventName'); + }); +}); diff --git a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue index 62f8b001a..e33dcbcf4 100644 --- a/app/javascript/dashboard/modules/contact/ContactMergeModal.vue +++ b/app/javascript/dashboard/modules/contact/ContactMergeModal.vue @@ -24,9 +24,7 @@ import MergeContact from 'dashboard/modules/contact/components/MergeContact'; import ContactAPI from 'dashboard/api/contacts'; import { mapGetters } from 'vuex'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../helper/AnalyticsHelper'; +import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events'; export default { components: { MergeContact }, @@ -75,7 +73,7 @@ export default { } }, async onMergeContacts(childContactId) { - AnalyticsHelper.track(ANALYTICS_EVENTS.MERGED_CONTACTS); + this.$track(CONTACTS_EVENTS.MERGED_CONTACTS); try { await this.$store.dispatch('contacts/merge', { childId: childContactId, diff --git a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue index 17fb5ab7e..7070a53b4 100644 --- a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue +++ b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue @@ -72,9 +72,7 @@ import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; import { copyTextToClipboard } from 'shared/helpers/clipboard'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../helper/AnalyticsHelper'; +import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events'; export default { components: { @@ -130,7 +128,7 @@ export default { this.$emit('toggle', false); }, showCannedResponseModal() { - AnalyticsHelper.track(ANALYTICS_EVENTS.ADDED_TO_CANNED_RESPONSE); + this.$track(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE); this.isCannedResponseModalOpen = true; }, }, diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue index 95c9ae61c..3e988e0a4 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue @@ -70,6 +70,7 @@ import { mapGetters } from 'vuex'; import { filterAttributeGroups } from '../contactFilterItems'; import filterMixin from 'shared/mixins/filterMixin'; import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; +import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; export default { components: { FilterInputBox, @@ -251,6 +252,12 @@ export default { JSON.parse(JSON.stringify(this.appliedFilters)) ); this.$emit('applyFilter', this.appliedFilters); + this.$track(CONTACTS_EVENTS.APPLY_FILTER, { + applied_filters: this.appliedFilters.map(filter => ({ + key: filter.attribute_key, + operator: filter.filter_operator, + })), + }); }, resetFilter(index, currentFilter) { this.appliedFilters[index].filter_operator = this.filterTypes.find( diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index e09c4a2d8..afbcfd5a5 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -86,6 +86,7 @@ import contactFilterItems from '../contactFilterItems'; import filterQueryGenerator from '../../../../helper/filterQueryGenerator'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews'; +import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; const DEFAULT_PAGE = 1; const FILTER_TYPE_CONTACT = 1; @@ -334,6 +335,14 @@ export default { onSortChange(params) { this.sortConfig = params; this.fetchContacts(this.meta.currentPage); + + const sortBy = + Object.entries(params).find(pair => Boolean(pair[1])) || []; + + this.$track(CONTACTS_EVENTS.APPLY_SORT, { + appliedOn: sortBy[0], + order: sortBy[1], + }); }, onToggleFilters() { this.showFiltersModal = !this.showFiltersModal; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue index a8e582bcf..91393d334 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ImportContacts.vue @@ -45,6 +45,7 @@ import Modal from '../../../../components/Modal'; import { mapGetters } from 'vuex'; import alertMixin from 'shared/mixins/alertMixin'; +import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; export default { components: { @@ -71,6 +72,9 @@ export default { return '/downloads/import-contacts-sample.csv'; }, }, + mounted() { + this.$track(CONTACTS_EVENTS.IMPORT_MODAL_OPEN); + }, methods: { async uploadFile() { try { @@ -78,10 +82,12 @@ export default { await this.$store.dispatch('contacts/import', this.file); this.onClose(); this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE')); + this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS); } catch (error) { this.showAlert( error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE') ); + this.$track(CONTACTS_EVENTS.IMPORT_FAILURE); } }, handleFileUpload() { diff --git a/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue index e013af14c..f683690d4 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/Macros/MacroItem.vue @@ -35,9 +35,8 @@ import alertMixin from 'shared/mixins/alertMixin'; import { mixin as clickaway } from 'vue-clickaway'; import MacroPreview from './MacroPreview'; -import AnalyticsHelper, { - ANALYTICS_EVENTS, -} from '../../../../helper/AnalyticsHelper'; +import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events'; + export default { components: { MacroPreview, @@ -67,7 +66,7 @@ export default { macroId: macro.id, conversationIds: [this.conversationId], }); - AnalyticsHelper.track(ANALYTICS_EVENTS.EXECUTED_A_MACRO); + this.$track(CONVERSATION_EVENTS.EXECUTED_A_MACRO); this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY')); } catch (error) { this.showAlert(this.$t('MACROS.ERROR')); diff --git a/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue b/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue index 37e532e02..c87c85883 100644 --- a/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue +++ b/app/javascript/dashboard/routes/dashboard/customviews/AddCustomViews.vue @@ -31,6 +31,7 @@