Merge branch 'develop' into chore-replace-router-mixin
This commit is contained in:
@@ -4,6 +4,7 @@ import { mapGetters } from 'vuex';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import VirtualList from 'vue-virtual-scroll-list';
|
||||
|
||||
import ChatListHeader from './ChatListHeader.vue';
|
||||
@@ -16,7 +17,6 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
@@ -41,7 +41,6 @@ export default {
|
||||
IntersectionObserver,
|
||||
VirtualList,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
provide() {
|
||||
return {
|
||||
// Actions to be performed on virtual list item and context menu.
|
||||
@@ -91,6 +90,15 @@ export default {
|
||||
|
||||
const conversationListRef = ref(null);
|
||||
|
||||
const {
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
} = useFilter({
|
||||
filteri18nKey: 'FILTER',
|
||||
attributeModel: 'conversation_attribute',
|
||||
});
|
||||
|
||||
const getKeyboardListenerParams = () => {
|
||||
const allConversations = conversationListRef.value.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
@@ -146,6 +154,9 @@ export default {
|
||||
return {
|
||||
uiSettings,
|
||||
conversationListRef,
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -860,6 +871,25 @@ export default {
|
||||
onContextMenuToggle(state) {
|
||||
this.isContextMenuOpen = state;
|
||||
},
|
||||
initializeExistingFilterToModal() {
|
||||
const statusFilter = this.initializeStatusAndAssigneeFilterToModal(
|
||||
this.activeStatus,
|
||||
this.currentUserDetails,
|
||||
this.activeAssigneeTab
|
||||
);
|
||||
if (statusFilter) {
|
||||
this.appliedFilter.push(statusFilter);
|
||||
}
|
||||
|
||||
const otherFilters = this.initializeInboxTeamAndLabelFilterToModal(
|
||||
this.conversationInbox,
|
||||
this.inbox,
|
||||
this.teamId,
|
||||
this.activeTeam,
|
||||
this.label
|
||||
);
|
||||
this.appliedFilter.push(...otherFilters);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import AILoader from './AILoader.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AILoader,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
aiOption: {
|
||||
type: String,
|
||||
@@ -15,12 +14,9 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
const { draftMessage, processEvent, recordAnalytics } = useAI();
|
||||
return {
|
||||
draftMessage,
|
||||
processEvent,
|
||||
recordAnalytics,
|
||||
};
|
||||
return { draftMessage, processEvent, recordAnalytics, formatMessage };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
+15
-3
@@ -5,7 +5,7 @@ import languages from './advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { filterAttributeGroups } from './advancedFilterItems';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
||||
@@ -14,7 +14,6 @@ export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -37,6 +36,15 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { setFilterAttributes } = useFilter({
|
||||
filteri18nKey: 'FILTER',
|
||||
attributeModel: 'conversation_attribute',
|
||||
});
|
||||
return {
|
||||
setFilterAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
@@ -67,7 +75,11 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
||||
|
||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
||||
this.filterGroups = filterGroups;
|
||||
|
||||
this.$store.dispatch('campaigns/get');
|
||||
if (this.getAppliedConversationFilters.length) {
|
||||
this.appliedFilters = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import BubbleActions from './bubble/Actions.vue';
|
||||
import BubbleContact from './bubble/Contact.vue';
|
||||
import BubbleFile from './bubble/File.vue';
|
||||
@@ -39,7 +39,6 @@ export default {
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
@@ -74,6 +73,12 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showContextMenu: false,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script>
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { ATTACHMENT_ICONS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
name: 'MessagePreview',
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -20,6 +19,12 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getPlainText } = useMessageFormatter();
|
||||
return {
|
||||
getPlainText,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageByAgent() {
|
||||
const { message_type: messageType } = this.message;
|
||||
|
||||
@@ -17,7 +17,6 @@ import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
@@ -61,12 +60,7 @@ export default {
|
||||
MessageSignatureMissingAlert,
|
||||
ArticleSearchPopover,
|
||||
},
|
||||
mixins: [
|
||||
inboxMixin,
|
||||
messageFormatterMixin,
|
||||
fileUploadMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
popoutReplyBox: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export const summary = {
|
||||
avg_first_response_time: '198.6666666666667',
|
||||
avg_resolution_time: '208.3333333333333',
|
||||
conversations_count: 5000,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 3,
|
||||
previous: {
|
||||
avg_first_response_time: '89.0',
|
||||
avg_resolution_time: '145.0',
|
||||
conversations_count: 4,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 4,
|
||||
resolutions_count: 0,
|
||||
},
|
||||
resolutions_count: 3,
|
||||
};
|
||||
|
||||
export const botSummary = {
|
||||
bot_resolutions_count: 10,
|
||||
bot_handoffs_count: 20,
|
||||
previous: {
|
||||
bot_resolutions_count: 8,
|
||||
bot_handoffs_count: 5,
|
||||
},
|
||||
};
|
||||
|
||||
export const report = {
|
||||
data: [
|
||||
{ value: '0.00', timestamp: 1647541800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647628200, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647714600, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647801000, count: 0 },
|
||||
{ value: '0.01', timestamp: 1647887400, count: 4 },
|
||||
{ value: '0.00', timestamp: 1647973800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1648060200, count: 0 },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,295 @@
|
||||
import { useAutomation } from '../useAutomation';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from '../useI18n';
|
||||
import * as automationHelper from 'dashboard/helper/automationHelper';
|
||||
import {
|
||||
customAttributes,
|
||||
agents,
|
||||
teams,
|
||||
labels,
|
||||
statusFilterOptions,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
languages,
|
||||
countries,
|
||||
slaPolicies,
|
||||
} from 'dashboard/helper/specs/fixtures/automationFixtures.js';
|
||||
import { MESSAGE_CONDITION_VALUES } from 'dashboard/constants/automation';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables');
|
||||
vi.mock('../useI18n');
|
||||
vi.mock('dashboard/helper/automationHelper');
|
||||
|
||||
describe('useAutomation', () => {
|
||||
beforeEach(() => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
'attributes/getAttributes': { value: customAttributes },
|
||||
'attributes/getAttributesByModel': {
|
||||
value: model => {
|
||||
return model === 'conversation_attribute'
|
||||
? [{ id: 1, name: 'Conversation Attribute' }]
|
||||
: [{ id: 2, name: 'Contact Attribute' }];
|
||||
},
|
||||
},
|
||||
});
|
||||
useMapGetter.mockImplementation(getter => {
|
||||
const getterMap = {
|
||||
'agents/getAgents': agents,
|
||||
'campaigns/getAllCampaigns': campaigns,
|
||||
'contacts/getContacts': contacts,
|
||||
'inboxes/getInboxes': inboxes,
|
||||
'labels/getLabels': labels,
|
||||
'teams/getTeams': teams,
|
||||
'sla/getSLA': slaPolicies,
|
||||
};
|
||||
return { value: getterMap[getter] };
|
||||
});
|
||||
useI18n.mockReturnValue({ t: key => key });
|
||||
useAlert.mockReturnValue(vi.fn());
|
||||
|
||||
// Mock getConditionOptions for different types
|
||||
automationHelper.getConditionOptions.mockImplementation(options => {
|
||||
const { type } = options;
|
||||
switch (type) {
|
||||
case 'status':
|
||||
return statusFilterOptions;
|
||||
case 'team_id':
|
||||
return teams;
|
||||
case 'assignee_id':
|
||||
return agents;
|
||||
case 'contact':
|
||||
return contacts;
|
||||
case 'inbox_id':
|
||||
return inboxes;
|
||||
case 'campaigns':
|
||||
return campaigns;
|
||||
case 'browser_language':
|
||||
return languages;
|
||||
case 'country_code':
|
||||
return countries;
|
||||
case 'message_type':
|
||||
return MESSAGE_CONDITION_VALUES;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// Mock getActionOptions for different types
|
||||
automationHelper.getActionOptions.mockImplementation(options => {
|
||||
const { type } = options;
|
||||
switch (type) {
|
||||
case 'add_label':
|
||||
return labels;
|
||||
case 'assign_team':
|
||||
return teams;
|
||||
case 'assign_agent':
|
||||
return agents;
|
||||
case 'send_email_to_team':
|
||||
return teams;
|
||||
case 'send_message':
|
||||
return [];
|
||||
case 'add_sla':
|
||||
return slaPolicies;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes computed properties correctly', () => {
|
||||
const {
|
||||
agents: computedAgents,
|
||||
campaigns: computedCampaigns,
|
||||
contacts: computedContacts,
|
||||
inboxes: computedInboxes,
|
||||
labels: computedLabels,
|
||||
teams: computedTeams,
|
||||
slaPolicies: computedSlaPolicies,
|
||||
} = useAutomation();
|
||||
|
||||
expect(computedAgents.value).toEqual(agents);
|
||||
expect(computedCampaigns.value).toEqual(campaigns);
|
||||
expect(computedContacts.value).toEqual(contacts);
|
||||
expect(computedInboxes.value).toEqual(inboxes);
|
||||
expect(computedLabels.value).toEqual(labels);
|
||||
expect(computedTeams.value).toEqual(teams);
|
||||
expect(computedSlaPolicies.value).toEqual(slaPolicies);
|
||||
});
|
||||
|
||||
it('appends new condition and action correctly', () => {
|
||||
const { appendNewCondition, appendNewAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
event_name: 'message_created',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
automationHelper.getDefaultConditions.mockReturnValue([{}]);
|
||||
automationHelper.getDefaultActions.mockReturnValue([{}]);
|
||||
|
||||
appendNewCondition(mockAutomation);
|
||||
appendNewAction(mockAutomation);
|
||||
|
||||
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
|
||||
'message_created'
|
||||
);
|
||||
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('removes filter and action correctly', () => {
|
||||
const { removeFilter, removeAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
conditions: [{ id: 1 }, { id: 2 }],
|
||||
actions: [{ id: 1 }, { id: 2 }],
|
||||
};
|
||||
|
||||
removeFilter(mockAutomation, 0);
|
||||
removeAction(mockAutomation, 0);
|
||||
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
expect(mockAutomation.conditions[0].id).toBe(2);
|
||||
expect(mockAutomation.actions[0].id).toBe(2);
|
||||
});
|
||||
|
||||
it('resets filter and action correctly', () => {
|
||||
const { resetFilter, resetAction } = useAutomation();
|
||||
const mockAutomation = {
|
||||
event_name: 'message_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: 'open',
|
||||
},
|
||||
],
|
||||
actions: [{ action_name: 'assign_agent', action_params: [1] }],
|
||||
};
|
||||
const mockAutomationTypes = {
|
||||
message_created: {
|
||||
conditions: [
|
||||
{ key: 'status', filterOperators: [{ value: 'not_equal_to' }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
resetFilter(
|
||||
mockAutomation,
|
||||
mockAutomationTypes,
|
||||
0,
|
||||
mockAutomation.conditions[0]
|
||||
);
|
||||
resetAction(mockAutomation, 0);
|
||||
|
||||
expect(mockAutomation.conditions[0].filter_operator).toBe('not_equal_to');
|
||||
expect(mockAutomation.conditions[0].values).toBe('');
|
||||
expect(mockAutomation.actions[0].action_params).toEqual([]);
|
||||
});
|
||||
|
||||
it('formats automation correctly', () => {
|
||||
const { formatAutomation } = useAutomation();
|
||||
const mockAutomation = {
|
||||
conditions: [{ attribute_key: 'status', values: ['open'] }],
|
||||
actions: [{ action_name: 'assign_agent', action_params: [1] }],
|
||||
};
|
||||
const mockAutomationTypes = {};
|
||||
const mockAutomationActionTypes = [
|
||||
{ key: 'assign_agent', inputType: 'search_select' },
|
||||
];
|
||||
|
||||
automationHelper.getConditionOptions.mockReturnValue([
|
||||
{ id: 'open', name: 'open' },
|
||||
]);
|
||||
automationHelper.getActionOptions.mockReturnValue([
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
]);
|
||||
|
||||
const result = formatAutomation(
|
||||
mockAutomation,
|
||||
customAttributes,
|
||||
mockAutomationTypes,
|
||||
mockAutomationActionTypes
|
||||
);
|
||||
|
||||
expect(result.conditions[0].values).toEqual([{ id: 'open', name: 'open' }]);
|
||||
expect(result.actions[0].action_params).toEqual([
|
||||
{ id: 1, name: 'Agent 1' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('manifests custom attributes correctly', () => {
|
||||
const { manifestCustomAttributes } = useAutomation();
|
||||
const mockAutomationTypes = {
|
||||
message_created: { conditions: [] },
|
||||
conversation_created: { conditions: [] },
|
||||
conversation_updated: { conditions: [] },
|
||||
conversation_opened: { conditions: [] },
|
||||
};
|
||||
|
||||
automationHelper.generateCustomAttributeTypes.mockReturnValue([]);
|
||||
automationHelper.generateCustomAttributes.mockReturnValue([]);
|
||||
|
||||
manifestCustomAttributes(mockAutomationTypes);
|
||||
|
||||
expect(automationHelper.generateCustomAttributeTypes).toHaveBeenCalledTimes(
|
||||
2
|
||||
);
|
||||
expect(automationHelper.generateCustomAttributes).toHaveBeenCalledTimes(1);
|
||||
Object.values(mockAutomationTypes).forEach(type => {
|
||||
expect(type.conditions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('gets condition dropdown values correctly', () => {
|
||||
const { getConditionDropdownValues } = useAutomation();
|
||||
|
||||
expect(getConditionDropdownValues('status')).toEqual(statusFilterOptions);
|
||||
expect(getConditionDropdownValues('team_id')).toEqual(teams);
|
||||
expect(getConditionDropdownValues('assignee_id')).toEqual(agents);
|
||||
expect(getConditionDropdownValues('contact')).toEqual(contacts);
|
||||
expect(getConditionDropdownValues('inbox_id')).toEqual(inboxes);
|
||||
expect(getConditionDropdownValues('campaigns')).toEqual(campaigns);
|
||||
expect(getConditionDropdownValues('browser_language')).toEqual(languages);
|
||||
expect(getConditionDropdownValues('country_code')).toEqual(countries);
|
||||
expect(getConditionDropdownValues('message_type')).toEqual(
|
||||
MESSAGE_CONDITION_VALUES
|
||||
);
|
||||
});
|
||||
|
||||
it('gets action dropdown values correctly', () => {
|
||||
const { getActionDropdownValues } = useAutomation();
|
||||
|
||||
expect(getActionDropdownValues('add_label')).toEqual(labels);
|
||||
expect(getActionDropdownValues('assign_team')).toEqual(teams);
|
||||
expect(getActionDropdownValues('assign_agent')).toEqual(agents);
|
||||
expect(getActionDropdownValues('send_email_to_team')).toEqual(teams);
|
||||
expect(getActionDropdownValues('send_message')).toEqual([]);
|
||||
expect(getActionDropdownValues('add_sla')).toEqual(slaPolicies);
|
||||
});
|
||||
|
||||
it('handles event change correctly', () => {
|
||||
const { onEventChange } = useAutomation();
|
||||
const mockAutomation = {
|
||||
event_name: 'message_created',
|
||||
conditions: [],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
automationHelper.getDefaultConditions.mockReturnValue([{}]);
|
||||
automationHelper.getDefaultActions.mockReturnValue([{}]);
|
||||
|
||||
onEventChange(mockAutomation);
|
||||
|
||||
expect(automationHelper.getDefaultConditions).toHaveBeenCalledWith(
|
||||
'message_created'
|
||||
);
|
||||
expect(automationHelper.getDefaultActions).toHaveBeenCalled();
|
||||
expect(mockAutomation.conditions).toHaveLength(1);
|
||||
expect(mockAutomation.actions).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useMacros } from '../useMacros';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/constants/automation';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/helper/automationHelper.js');
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ref } from 'vue';
|
||||
import { useReportMetrics } from '../useReportMetrics';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { summary, botSummary } from './fixtures/reportFixtures';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('@chatwoot/utils', () => ({
|
||||
formatTime: vi.fn(time => `formatted_${time}`),
|
||||
}));
|
||||
|
||||
describe('useReportMetrics', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useMapGetter.mockReturnValue(ref(summary));
|
||||
});
|
||||
|
||||
it('calculates trend correctly', () => {
|
||||
const { calculateTrend } = useReportMetrics();
|
||||
|
||||
expect(calculateTrend('conversations_count')).toBe(124900);
|
||||
expect(calculateTrend('incoming_messages_count')).toBe(0);
|
||||
expect(calculateTrend('avg_first_response_time')).toBe(123);
|
||||
});
|
||||
|
||||
it('returns 0 for trend when previous value is not available', () => {
|
||||
const { calculateTrend } = useReportMetrics();
|
||||
|
||||
expect(calculateTrend('non_existent_key')).toBe(0);
|
||||
});
|
||||
|
||||
it('identifies average metric types correctly', () => {
|
||||
const { isAverageMetricType } = useReportMetrics();
|
||||
|
||||
expect(isAverageMetricType('avg_first_response_time')).toBe(true);
|
||||
expect(isAverageMetricType('avg_resolution_time')).toBe(true);
|
||||
expect(isAverageMetricType('reply_time')).toBe(true);
|
||||
expect(isAverageMetricType('conversations_count')).toBe(false);
|
||||
});
|
||||
|
||||
it('displays metrics correctly for account', () => {
|
||||
const { displayMetric } = useReportMetrics();
|
||||
|
||||
expect(displayMetric('conversations_count')).toBe('5,000');
|
||||
expect(displayMetric('incoming_messages_count')).toBe('5');
|
||||
});
|
||||
|
||||
it('displays the metric for bot', () => {
|
||||
const customKey = 'getBotSummary';
|
||||
useMapGetter.mockReturnValue(ref(botSummary));
|
||||
const { displayMetric } = useReportMetrics(customKey);
|
||||
|
||||
expect(displayMetric('bot_resolutions_count')).toBe('10');
|
||||
expect(displayMetric('bot_handoffs_count')).toBe('20');
|
||||
});
|
||||
|
||||
it('handles non-existent metrics', () => {
|
||||
const { displayMetric } = useReportMetrics();
|
||||
|
||||
expect(displayMetric('non_existent_key')).toBe('0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from './useI18n';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import {
|
||||
generateCustomAttributeTypes,
|
||||
getActionOptions,
|
||||
getConditionOptions,
|
||||
getCustomAttributeInputType,
|
||||
getDefaultConditions,
|
||||
getDefaultActions,
|
||||
filterCustomAttributes,
|
||||
getStandardAttributeInputType,
|
||||
isCustomAttribute,
|
||||
generateCustomAttributes,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
|
||||
/**
|
||||
* Composable for handling automation-related functionality.
|
||||
* @returns {Object} An object containing various automation-related functions and computed properties.
|
||||
*/
|
||||
export function useAutomation() {
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agents = useMapGetter('agents/getAgents');
|
||||
const campaigns = useMapGetter('campaigns/getAllCampaigns');
|
||||
const contacts = useMapGetter('contacts/getContacts');
|
||||
const inboxes = useMapGetter('inboxes/getInboxes');
|
||||
const labels = useMapGetter('labels/getLabels');
|
||||
const teams = useMapGetter('teams/getTeams');
|
||||
const slaPolicies = useMapGetter('sla/getSLA');
|
||||
|
||||
const booleanFilterOptions = computed(() => [
|
||||
{ id: true, name: t('FILTER.ATTRIBUTE_LABELS.TRUE') },
|
||||
{ id: false, name: t('FILTER.ATTRIBUTE_LABELS.FALSE') },
|
||||
]);
|
||||
|
||||
const statusFilterOptions = computed(() => {
|
||||
const statusFilters = t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => ({
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
})),
|
||||
{ id: 'all', name: t('CHAT_LIST.FILTER_ALL') },
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the event change for an automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const onEventChange = automation => {
|
||||
automation.conditions = getDefaultConditions(automation.event_name);
|
||||
automation.actions = getDefaultActions();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the condition dropdown values for a given type.
|
||||
* @param {string} type - The type of condition.
|
||||
* @returns {Array} An array of condition dropdown values.
|
||||
*/
|
||||
const getConditionDropdownValues = type => {
|
||||
return getConditionOptions({
|
||||
agents: agents.value,
|
||||
booleanFilterOptions: booleanFilterOptions.value,
|
||||
campaigns: campaigns.value,
|
||||
contacts: contacts.value,
|
||||
customAttributes: getters['attributes/getAttributes'].value,
|
||||
inboxes: inboxes.value,
|
||||
statusFilterOptions: statusFilterOptions.value,
|
||||
teams: teams.value,
|
||||
languages,
|
||||
countries,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a new condition to the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const appendNewCondition = automation => {
|
||||
automation.conditions.push(...getDefaultConditions(automation.event_name));
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a new action to the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
*/
|
||||
const appendNewAction = automation => {
|
||||
automation.actions.push(...getDefaultActions());
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a filter from the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {number} index - The index of the filter to remove.
|
||||
*/
|
||||
const removeFilter = (automation, index) => {
|
||||
if (automation.conditions.length <= 1) {
|
||||
useAlert(t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
automation.conditions.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes an action from the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {number} index - The index of the action to remove.
|
||||
*/
|
||||
const removeAction = (automation, index) => {
|
||||
if (automation.actions.length <= 1) {
|
||||
useAlert(t('AUTOMATION.ACTION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
automation.actions.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets a filter in the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {Object} automationTypes - The automation types object.
|
||||
* @param {number} index - The index of the filter to reset.
|
||||
* @param {Object} currentCondition - The current condition object.
|
||||
*/
|
||||
const resetFilter = (
|
||||
automation,
|
||||
automationTypes,
|
||||
index,
|
||||
currentCondition
|
||||
) => {
|
||||
automation.conditions[index].filter_operator = automationTypes[
|
||||
automation.event_name
|
||||
].conditions.find(
|
||||
condition => condition.key === currentCondition.attribute_key
|
||||
).filterOperators[0].value;
|
||||
automation.conditions[index].values = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets an action in the automation.
|
||||
* @param {Object} automation - The automation object to update.
|
||||
* @param {number} index - The index of the action to reset.
|
||||
*/
|
||||
const resetAction = (automation, index) => {
|
||||
automation.actions[index].action_params = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the conditions for automation.
|
||||
* It help to format the conditions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing conditions to manifest.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @returns {Array} An array of manifested conditions.
|
||||
*/
|
||||
const manifestConditions = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
) => {
|
||||
const customAttributes = filterCustomAttributes(allCustomAttributes);
|
||||
return automation.conditions.map(condition => {
|
||||
const customAttr = isCustomAttribute(
|
||||
customAttributes,
|
||||
condition.attribute_key
|
||||
);
|
||||
let inputType = 'plain_text';
|
||||
if (customAttr) {
|
||||
inputType = getCustomAttributeInputType(customAttr.type);
|
||||
} else {
|
||||
inputType = getStandardAttributeInputType(
|
||||
automationTypes,
|
||||
automation.event_name,
|
||||
condition.attribute_key
|
||||
);
|
||||
}
|
||||
if (inputType === 'plain_text' || inputType === 'date') {
|
||||
return { ...condition, values: condition.values[0] };
|
||||
}
|
||||
if (inputType === 'comma_separated_plain_text') {
|
||||
return { ...condition, values: condition.values.join(',') };
|
||||
}
|
||||
return {
|
||||
...condition,
|
||||
query_operator: condition.query_operator || 'and',
|
||||
values: [...getConditionDropdownValues(condition.attribute_key)].filter(
|
||||
item => [...condition.values].includes(item.id)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the action dropdown values for a given type.
|
||||
* @param {string} type - The type of action.
|
||||
* @returns {Array} An array of action dropdown values.
|
||||
*/
|
||||
const getActionDropdownValues = type => {
|
||||
return getActionOptions({
|
||||
agents: agents.value,
|
||||
labels: labels.value,
|
||||
teams: teams.value,
|
||||
slaPolicies: slaPolicies.value,
|
||||
languages,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of actions for the automation.
|
||||
* @param {Object} action - The action object.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array|Object} Generated actions array or object based on input type.
|
||||
*/
|
||||
const generateActionsArray = (action, automationActionTypes) => {
|
||||
const params = action.action_params;
|
||||
const inputType = automationActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
return [...getActionDropdownValues(action.action_name)].filter(item =>
|
||||
[...params].includes(item.id)
|
||||
);
|
||||
}
|
||||
if (inputType === 'team_message') {
|
||||
return {
|
||||
team_ids: [...getActionDropdownValues(action.action_name)].filter(
|
||||
item => [...params[0].team_ids].includes(item.id)
|
||||
),
|
||||
message: params[0].message,
|
||||
};
|
||||
}
|
||||
return [...params];
|
||||
};
|
||||
|
||||
/**
|
||||
* This function sets the actions for automation.
|
||||
* It help to format the actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object containing actions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Array} An array of manifested actions.
|
||||
*/
|
||||
const manifestActions = (automation, automationActionTypes) => {
|
||||
return automation.actions.map(action => ({
|
||||
...action,
|
||||
action_params: action.action_params.length
|
||||
? generateActionsArray(action, automationActionTypes)
|
||||
: [],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the automation object for use when we edit the automation.
|
||||
* It help to format the conditions and actions for the automation when we open the edit automation modal.
|
||||
* @param {Object} automation - The automation object to format.
|
||||
* @param {Array} allCustomAttributes - List of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation type definitions.
|
||||
* @param {Array} automationActionTypes - List of available automation action types.
|
||||
* @returns {Object} A new object with formatted automation data, including automation conditions and actions.
|
||||
*/
|
||||
const formatAutomation = (
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automationActionTypes
|
||||
) => {
|
||||
return {
|
||||
...automation,
|
||||
conditions: manifestConditions(
|
||||
automation,
|
||||
allCustomAttributes,
|
||||
automationTypes
|
||||
),
|
||||
actions: manifestActions(automation, automationActionTypes),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This function formats the custom attributes for automation types.
|
||||
* It retrieves custom attributes for conversations and contacts,
|
||||
* generates custom attribute types, and adds them to the relevant automation types.
|
||||
* @param {Object} automationTypes - The automation types object to update with custom attributes.
|
||||
*/
|
||||
const manifestCustomAttributes = automationTypes => {
|
||||
const conversationCustomAttributesRaw = getters[
|
||||
'attributes/getAttributesByModel'
|
||||
].value('conversation_attribute');
|
||||
const contactCustomAttributesRaw =
|
||||
getters['attributes/getAttributesByModel'].value('contact_attribute');
|
||||
|
||||
const conversationCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
conversationCustomAttributesRaw,
|
||||
'conversation_attribute'
|
||||
);
|
||||
const contactCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
contactCustomAttributesRaw,
|
||||
'contact_attribute'
|
||||
);
|
||||
|
||||
const manifestedCustomAttributes = generateCustomAttributes(
|
||||
conversationCustomAttributeTypes,
|
||||
contactCustomAttributeTypes,
|
||||
t('AUTOMATION.CONDITION.CONVERSATION_CUSTOM_ATTR_LABEL'),
|
||||
t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
|
||||
);
|
||||
|
||||
automationTypes.message_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_updated.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
automationTypes.conversation_opened.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
agents,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
labels,
|
||||
teams,
|
||||
slaPolicies,
|
||||
booleanFilterOptions,
|
||||
statusFilterOptions,
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
formatAutomation,
|
||||
getActionDropdownValues,
|
||||
manifestCustomAttributes,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/helper/automationHelper.js';
|
||||
import { PRIORITY_CONDITION_VALUES } from 'dashboard/constants/automation';
|
||||
|
||||
/**
|
||||
* Composable for handling macro-related functionality
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
/**
|
||||
* A composable function for report metrics calculations and display.
|
||||
*
|
||||
* @param {string} [accountSummaryKey='getAccountSummary'] - The key for accessing account summary data.
|
||||
* @returns {Object} An object containing utility functions for report metrics.
|
||||
*/
|
||||
export function useReportMetrics(accountSummaryKey = 'getAccountSummary') {
|
||||
const accountSummary = useMapGetter(accountSummaryKey);
|
||||
|
||||
/**
|
||||
* Calculates the trend percentage for a given metric.
|
||||
*
|
||||
* @param {string} key - The key of the metric to calculate trend for.
|
||||
* @returns {number} The calculated trend percentage, rounded to the nearest integer.
|
||||
*/
|
||||
const calculateTrend = key => {
|
||||
if (!accountSummary.value.previous[key]) return 0;
|
||||
const diff = accountSummary.value[key] - accountSummary.value.previous[key];
|
||||
return Math.round((diff / accountSummary.value.previous[key]) * 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a given metric key represents an average metric type.
|
||||
*
|
||||
* @param {string} key - The key of the metric to check.
|
||||
* @returns {boolean} True if the metric is an average type, false otherwise.
|
||||
*/
|
||||
const isAverageMetricType = key => {
|
||||
return [
|
||||
'avg_first_response_time',
|
||||
'avg_resolution_time',
|
||||
'reply_time',
|
||||
].includes(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats and displays a metric value based on its type.
|
||||
*
|
||||
* @param {string} key - The key of the metric to display.
|
||||
* @returns {string} The formatted metric value as a string.
|
||||
*/
|
||||
const displayMetric = key => {
|
||||
if (isAverageMetricType(key)) {
|
||||
return formatTime(accountSummary.value[key]);
|
||||
}
|
||||
return Number(accountSummary.value[key] || '').toLocaleString();
|
||||
};
|
||||
|
||||
return {
|
||||
calculateTrend,
|
||||
isAverageMetricType,
|
||||
displayMetric,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
export const DEFAULT_MESSAGE_CREATED_CONDITION = [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_CONVERSATION_OPENED_CONDITION = [
|
||||
{
|
||||
attribute_key: 'browser_language',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_OTHER_CONDITION = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_ACTIONS = [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const MESSAGE_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'incoming',
|
||||
name: 'Incoming Message',
|
||||
},
|
||||
{
|
||||
id: 'outgoing',
|
||||
name: 'Outgoing Message',
|
||||
},
|
||||
];
|
||||
|
||||
export const PRIORITY_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
name: 'High',
|
||||
},
|
||||
{
|
||||
id: 'urgent',
|
||||
name: 'Urgent',
|
||||
},
|
||||
];
|
||||
@@ -3,41 +3,16 @@ import {
|
||||
OPERATOR_TYPES_3,
|
||||
OPERATOR_TYPES_4,
|
||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||
import {
|
||||
DEFAULT_MESSAGE_CREATED_CONDITION,
|
||||
DEFAULT_CONVERSATION_OPENED_CONDITION,
|
||||
DEFAULT_OTHER_CONDITION,
|
||||
DEFAULT_ACTIONS,
|
||||
MESSAGE_CONDITION_VALUES,
|
||||
PRIORITY_CONDITION_VALUES,
|
||||
} from 'dashboard/constants/automation';
|
||||
import filterQueryGenerator from './filterQueryGenerator';
|
||||
import actionQueryGenerator from './actionQueryGenerator';
|
||||
const MESSAGE_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'incoming',
|
||||
name: 'Incoming Message',
|
||||
},
|
||||
{
|
||||
id: 'outgoing',
|
||||
name: 'Outgoing Message',
|
||||
},
|
||||
];
|
||||
|
||||
export const PRIORITY_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
name: 'High',
|
||||
},
|
||||
{
|
||||
id: 'urgent',
|
||||
name: 'Urgent',
|
||||
},
|
||||
];
|
||||
|
||||
export const getCustomAttributeInputType = key => {
|
||||
const customAttributeMap = {
|
||||
@@ -198,45 +173,16 @@ export const getFileName = (action, files = []) => {
|
||||
|
||||
export const getDefaultConditions = eventName => {
|
||||
if (eventName === 'message_created') {
|
||||
return [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_MESSAGE_CREATED_CONDITION;
|
||||
}
|
||||
if (eventName === 'conversation_opened') {
|
||||
return [
|
||||
{
|
||||
attribute_key: 'browser_language',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_CONVERSATION_OPENED_CONDITION;
|
||||
}
|
||||
return [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_OTHER_CONDITION;
|
||||
};
|
||||
|
||||
export const getDefaultActions = () => {
|
||||
return [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
},
|
||||
];
|
||||
return DEFAULT_ACTIONS;
|
||||
};
|
||||
|
||||
export const filterCustomAttributes = customAttributes => {
|
||||
@@ -297,3 +243,100 @@ export const generateCustomAttributes = (
|
||||
}
|
||||
return customAttributes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get attributes for a given key from automation types.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {string} key - The key to get attributes for.
|
||||
* @returns {Array} Array of condition objects for the given key.
|
||||
*/
|
||||
export const getAttributes = (automationTypes, key) => {
|
||||
return automationTypes[key].conditions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the automation type for a given key.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the automation type for.
|
||||
* @returns {Object} The automation type object.
|
||||
*/
|
||||
export const getAutomationType = (automationTypes, automation, key) => {
|
||||
return automationTypes[automation.event_name].conditions.find(
|
||||
condition => condition.key === key
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the input type for a given key.
|
||||
* @param {Array} allCustomAttributes - Array of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the input type for.
|
||||
* @returns {string} The input type.
|
||||
*/
|
||||
export const getInputType = (
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
key
|
||||
) => {
|
||||
const customAttribute = isACustomAttribute(allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getCustomAttributeInputType(customAttribute.attribute_display_type);
|
||||
}
|
||||
const type = getAutomationType(automationTypes, automation, key);
|
||||
return type.inputType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get operators for a given key.
|
||||
* @param {Array} allCustomAttributes - Array of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} mode - The mode ('edit' or other).
|
||||
* @param {string} key - The key to get operators for.
|
||||
* @returns {Array} Array of operators.
|
||||
*/
|
||||
export const getOperators = (
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
mode,
|
||||
key
|
||||
) => {
|
||||
if (mode === 'edit') {
|
||||
const customAttribute = isACustomAttribute(allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getOperatorTypes(customAttribute.attribute_display_type);
|
||||
}
|
||||
}
|
||||
const type = getAutomationType(automationTypes, automation, key);
|
||||
return type.filterOperators;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the custom attribute type for a given key.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the custom attribute type for.
|
||||
* @returns {string} The custom attribute type.
|
||||
*/
|
||||
export const getCustomAttributeType = (automationTypes, automation, key) => {
|
||||
return automationTypes[automation.event_name].conditions.find(
|
||||
i => i.key === key
|
||||
).customAttributeType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if an action input should be shown.
|
||||
* @param {Array} automationActionTypes - Array of automation action type objects.
|
||||
* @param {string} action - The action to check.
|
||||
* @returns {boolean} True if the action input should be shown, false otherwise.
|
||||
*/
|
||||
export const showActionInput = (automationActionTypes, action) => {
|
||||
if (action === 'send_email_to_team' || action === 'send_message')
|
||||
return false;
|
||||
const type = automationActionTypes.find(i => i.key === action).inputType;
|
||||
return !!type;
|
||||
};
|
||||
|
||||
+194
-102
@@ -11,11 +11,11 @@ import {
|
||||
contactAttrs,
|
||||
conversationAttrs,
|
||||
expectedOutputForCustomAttributeGenerator,
|
||||
} from './automationFixtures';
|
||||
} from './fixtures/automationFixtures';
|
||||
import { AUTOMATIONS } from 'dashboard/routes/dashboard/settings/automation/constants';
|
||||
|
||||
describe('automationMethodsMixin', () => {
|
||||
it('getCustomAttributeInputType returns the attribute input type', () => {
|
||||
describe('getCustomAttributeInputType', () => {
|
||||
it('returns the attribute input type', () => {
|
||||
expect(helpers.getCustomAttributeInputType('date')).toEqual('date');
|
||||
expect(helpers.getCustomAttributeInputType('date')).not.toEqual(
|
||||
'some_random_value'
|
||||
@@ -31,33 +31,32 @@ describe('automationMethodsMixin', () => {
|
||||
'plain_text'
|
||||
);
|
||||
});
|
||||
it('isACustomAttribute returns the custom attribute value if true', () => {
|
||||
});
|
||||
|
||||
describe('isACustomAttribute', () => {
|
||||
it('returns the custom attribute value if true', () => {
|
||||
expect(
|
||||
helpers.isACustomAttribute(customAttributes, 'signed_up_at')
|
||||
).toBeTruthy();
|
||||
expect(helpers.isACustomAttribute(customAttributes, 'status')).toBeFalsy();
|
||||
});
|
||||
it('getCustomAttributeListDropdownValues returns the attribute dropdown values', () => {
|
||||
});
|
||||
|
||||
describe('getCustomAttributeListDropdownValues', () => {
|
||||
it('returns the attribute dropdown values', () => {
|
||||
const myListValues = [
|
||||
{
|
||||
id: 'item1',
|
||||
name: 'item1',
|
||||
},
|
||||
{
|
||||
id: 'item2',
|
||||
name: 'item2',
|
||||
},
|
||||
{
|
||||
id: 'item3',
|
||||
name: 'item3',
|
||||
},
|
||||
{ id: 'item1', name: 'item1' },
|
||||
{ id: 'item2', name: 'item2' },
|
||||
{ id: 'item3', name: 'item3' },
|
||||
];
|
||||
expect(
|
||||
helpers.getCustomAttributeListDropdownValues(customAttributes, 'my_list')
|
||||
).toEqual(myListValues);
|
||||
});
|
||||
});
|
||||
|
||||
it('isCustomAttributeCheckbox checks if attribute is a checkbox', () => {
|
||||
describe('isCustomAttributeCheckbox', () => {
|
||||
it('checks if attribute is a checkbox', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'prime_user')
|
||||
.attribute_display_type
|
||||
@@ -70,13 +69,19 @@ describe('automationMethodsMixin', () => {
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'my_list')
|
||||
).not.toEqual('checkbox');
|
||||
});
|
||||
it('isCustomAttributeList checks if attribute is a list', () => {
|
||||
});
|
||||
|
||||
describe('isCustomAttributeList', () => {
|
||||
it('checks if attribute is a list', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeList(customAttributes, 'my_list')
|
||||
.attribute_display_type
|
||||
).toEqual('list');
|
||||
});
|
||||
it('getOperatorTypes returns the correct custom attribute operators', () => {
|
||||
});
|
||||
|
||||
describe('getOperatorTypes', () => {
|
||||
it('returns the correct custom attribute operators', () => {
|
||||
expect(helpers.getOperatorTypes('list')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('text')).toEqual(OPERATOR_TYPES_3);
|
||||
expect(helpers.getOperatorTypes('number')).toEqual(OPERATOR_TYPES_1);
|
||||
@@ -85,93 +90,44 @@ describe('automationMethodsMixin', () => {
|
||||
expect(helpers.getOperatorTypes('checkbox')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('some_random')).toEqual(OPERATOR_TYPES_1);
|
||||
});
|
||||
it('generateConditionOptions returns expected conditions options array', () => {
|
||||
const testConditions = [
|
||||
{
|
||||
id: 123,
|
||||
title: 'Fayaz',
|
||||
email: 'test@test.com',
|
||||
},
|
||||
{
|
||||
title: 'John',
|
||||
id: 324,
|
||||
email: 'test@john.com',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
describe('generateConditionOptions', () => {
|
||||
it('returns expected conditions options array', () => {
|
||||
const testConditions = [
|
||||
{ id: 123, title: 'Fayaz', email: 'test@test.com' },
|
||||
{ title: 'John', id: 324, email: 'test@john.com' },
|
||||
];
|
||||
const expectedConditions = [
|
||||
{
|
||||
id: 123,
|
||||
name: 'Fayaz',
|
||||
},
|
||||
{
|
||||
id: 324,
|
||||
name: 'John',
|
||||
},
|
||||
{ id: 123, name: 'Fayaz' },
|
||||
{ id: 324, name: 'John' },
|
||||
];
|
||||
expect(helpers.generateConditionOptions(testConditions)).toEqual(
|
||||
expectedConditions
|
||||
);
|
||||
});
|
||||
it('getActionOptions returns expected actions options array', () => {
|
||||
});
|
||||
|
||||
describe('getActionOptions', () => {
|
||||
it('returns expected actions options array', () => {
|
||||
const expectedOptions = [
|
||||
{
|
||||
id: 'testlabel',
|
||||
name: 'testlabel',
|
||||
},
|
||||
{
|
||||
id: 'snoozes',
|
||||
name: 'snoozes',
|
||||
},
|
||||
{ id: 'testlabel', name: 'testlabel' },
|
||||
{ id: 'snoozes', name: 'snoozes' },
|
||||
];
|
||||
expect(helpers.getActionOptions({ labels, type: 'add_label' })).toEqual(
|
||||
expectedOptions
|
||||
);
|
||||
});
|
||||
it('getConditionOptions returns expected conditions options', () => {
|
||||
});
|
||||
|
||||
describe('getConditionOptions', () => {
|
||||
it('returns expected conditions options', () => {
|
||||
const testOptions = [
|
||||
{
|
||||
id: 'open',
|
||||
name: 'Open',
|
||||
},
|
||||
{
|
||||
id: 'resolved',
|
||||
name: 'Resolved',
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
name: 'Pending',
|
||||
},
|
||||
{
|
||||
id: 'snoozed',
|
||||
name: 'Snoozed',
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All',
|
||||
},
|
||||
];
|
||||
const expectedOptions = [
|
||||
{
|
||||
id: 'open',
|
||||
name: 'Open',
|
||||
},
|
||||
{
|
||||
id: 'resolved',
|
||||
name: 'Resolved',
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
name: 'Pending',
|
||||
},
|
||||
{
|
||||
id: 'snoozed',
|
||||
name: 'Snoozed',
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All',
|
||||
},
|
||||
{ id: 'open', name: 'Open' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'snoozed', name: 'Snoozed' },
|
||||
{ id: 'all', name: 'All' },
|
||||
];
|
||||
expect(
|
||||
helpers.getConditionOptions({
|
||||
@@ -180,14 +136,20 @@ describe('automationMethodsMixin', () => {
|
||||
statusFilterOptions: testOptions,
|
||||
type: 'status',
|
||||
})
|
||||
).toEqual(expectedOptions);
|
||||
).toEqual(testOptions);
|
||||
});
|
||||
it('getFileName returns the correct file name', () => {
|
||||
});
|
||||
|
||||
describe('getFileName', () => {
|
||||
it('returns the correct file name', () => {
|
||||
expect(
|
||||
helpers.getFileName(automation.actions[0], automation.files)
|
||||
).toEqual('pfp.jpeg');
|
||||
});
|
||||
it('getDefaultConditions returns the resp default condition model', () => {
|
||||
});
|
||||
|
||||
describe('getDefaultConditions', () => {
|
||||
it('returns the resp default condition model', () => {
|
||||
const messageCreatedModel = [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
@@ -211,7 +173,10 @@ describe('automationMethodsMixin', () => {
|
||||
);
|
||||
expect(helpers.getDefaultConditions()).toEqual(genericConditionModel);
|
||||
});
|
||||
it('getDefaultActions returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('getDefaultActions', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const genericActionModel = [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
@@ -220,7 +185,10 @@ describe('automationMethodsMixin', () => {
|
||||
];
|
||||
expect(helpers.getDefaultActions()).toEqual(genericActionModel);
|
||||
});
|
||||
it('filterCustomAttributes filters the raw custom attributes', () => {
|
||||
});
|
||||
|
||||
describe('filterCustomAttributes', () => {
|
||||
it('filters the raw custom attributes', () => {
|
||||
const filteredAttributes = [
|
||||
{ key: 'signed_up_at', name: 'Signed Up At', type: 'date' },
|
||||
{ key: 'prime_user', name: 'Prime User', type: 'checkbox' },
|
||||
@@ -235,7 +203,10 @@ describe('automationMethodsMixin', () => {
|
||||
filteredAttributes
|
||||
);
|
||||
});
|
||||
it('getStandardAttributeInputType returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('getStandardAttributeInputType', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
@@ -258,7 +229,10 @@ describe('automationMethodsMixin', () => {
|
||||
)
|
||||
).toEqual('plain_text');
|
||||
});
|
||||
it('generateAutomationPayload returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('generateAutomationPayload', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const testPayload = {
|
||||
name: 'Test',
|
||||
description: 'This is a test',
|
||||
@@ -300,7 +274,10 @@ describe('automationMethodsMixin', () => {
|
||||
expectedPayload
|
||||
);
|
||||
});
|
||||
it('isCustomAttribute returns the resp default action model', () => {
|
||||
});
|
||||
|
||||
describe('isCustomAttribute', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const attrs = helpers.filterCustomAttributes(customAttributes);
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_list')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_check')).toBeTruthy();
|
||||
@@ -309,8 +286,10 @@ describe('automationMethodsMixin', () => {
|
||||
expect(helpers.isCustomAttribute(attrs, 'prime_user')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'hello')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('generateCustomAttributes generates and returns correct condition attribute', () => {
|
||||
describe('generateCustomAttributes', () => {
|
||||
it('generates and returns correct condition attribute', () => {
|
||||
expect(
|
||||
helpers.generateCustomAttributes(
|
||||
conversationAttrs,
|
||||
@@ -321,3 +300,116 @@ describe('automationMethodsMixin', () => {
|
||||
).toEqual(expectedOutputForCustomAttributeGenerator);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutomationType', () => {
|
||||
it('returns the automation type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getAutomationType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInputType', () => {
|
||||
it('returns the input type for a custom attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual('date');
|
||||
});
|
||||
|
||||
it('returns the input type for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual('search_select');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperators', () => {
|
||||
it('returns operators for a custom attribute in edit mode', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'edit',
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual(OPERATOR_TYPES_4);
|
||||
});
|
||||
|
||||
it('returns operators for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'create',
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.filterOperators
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomAttributeType', () => {
|
||||
it('returns the custom attribute type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getCustomAttributeType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.customAttributeType
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showActionInput', () => {
|
||||
it('returns false for send_email_to_team and send_message actions', () => {
|
||||
expect(helpers.showActionInput([], 'send_email_to_team')).toBe(false);
|
||||
expect(helpers.showActionInput([], 'send_message')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if the action has an input type', () => {
|
||||
const mockActionTypes = [{ key: 'add_label', inputType: 'select' }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'add_label')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the action does not have an input type', () => {
|
||||
const mockActionTypes = [{ key: 'some_action', inputType: null }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'some_action')).toBe(false);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import allLanguages from '../../../dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
|
||||
import allLanguages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
import allCountries from '../../../shared/constants/countries.js';
|
||||
import allCountries from 'shared/constants/countries.js';
|
||||
|
||||
export const customAttributes = [
|
||||
{
|
||||
@@ -1,314 +0,0 @@
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { validateAutomation } from 'dashboard/helper/validations';
|
||||
|
||||
import {
|
||||
generateCustomAttributeTypes,
|
||||
getActionOptions,
|
||||
getConditionOptions,
|
||||
getCustomAttributeInputType,
|
||||
getOperatorTypes,
|
||||
isACustomAttribute,
|
||||
getFileName,
|
||||
getDefaultConditions,
|
||||
getDefaultActions,
|
||||
filterCustomAttributes,
|
||||
generateAutomationPayload,
|
||||
getStandardAttributeInputType,
|
||||
isCustomAttribute,
|
||||
generateCustomAttributes,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agents: 'agents/getAgents',
|
||||
campaigns: 'campaigns/getAllCampaigns',
|
||||
contacts: 'contacts/getContacts',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
labels: 'labels/getLabels',
|
||||
teams: 'teams/getTeams',
|
||||
slaPolicies: 'sla/getSLA',
|
||||
}),
|
||||
booleanFilterOptions() {
|
||||
return [
|
||||
{
|
||||
id: true,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
statusFilterOptions() {
|
||||
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
return [
|
||||
...Object.keys(statusFilters).map(status => {
|
||||
return {
|
||||
id: status,
|
||||
name: statusFilters[status].TEXT,
|
||||
};
|
||||
}),
|
||||
{
|
||||
id: 'all',
|
||||
name: this.$t('CHAT_LIST.FILTER_ALL'),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFileName,
|
||||
onEventChange() {
|
||||
this.automation.conditions = getDefaultConditions(
|
||||
this.automation.event_name
|
||||
);
|
||||
this.automation.actions = getDefaultActions();
|
||||
},
|
||||
getAttributes(key) {
|
||||
return this.automationTypes[key].conditions;
|
||||
},
|
||||
getInputType(key) {
|
||||
const customAttribute = isACustomAttribute(this.allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getCustomAttributeInputType(
|
||||
customAttribute.attribute_display_type
|
||||
);
|
||||
}
|
||||
const type = this.getAutomationType(key);
|
||||
return type.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
if (this.mode === 'edit') {
|
||||
const customAttribute = isACustomAttribute(
|
||||
this.allCustomAttributes,
|
||||
key
|
||||
);
|
||||
if (customAttribute) {
|
||||
return getOperatorTypes(customAttribute.attribute_display_type);
|
||||
}
|
||||
}
|
||||
const type = this.getAutomationType(key);
|
||||
return type.filterOperators;
|
||||
},
|
||||
getAutomationType(key) {
|
||||
return this.automationTypes[this.automation.event_name].conditions.find(
|
||||
condition => condition.key === key
|
||||
);
|
||||
},
|
||||
getCustomAttributeType(key) {
|
||||
const type = this.automationTypes[
|
||||
this.automation.event_name
|
||||
].conditions.find(i => i.key === key).customAttributeType;
|
||||
return type;
|
||||
},
|
||||
getConditionDropdownValues(type) {
|
||||
const {
|
||||
agents,
|
||||
allCustomAttributes: customAttributes,
|
||||
booleanFilterOptions,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
statusFilterOptions,
|
||||
teams,
|
||||
} = this;
|
||||
return getConditionOptions({
|
||||
agents,
|
||||
booleanFilterOptions,
|
||||
campaigns,
|
||||
contacts,
|
||||
customAttributes,
|
||||
inboxes,
|
||||
statusFilterOptions,
|
||||
teams,
|
||||
languages,
|
||||
countries,
|
||||
type,
|
||||
});
|
||||
},
|
||||
appendNewCondition() {
|
||||
this.automation.conditions.push(
|
||||
...getDefaultConditions(this.automation.event_name)
|
||||
);
|
||||
},
|
||||
appendNewAction() {
|
||||
this.automation.actions.push(...getDefaultActions());
|
||||
},
|
||||
removeFilter(index) {
|
||||
if (this.automation.conditions.length <= 1) {
|
||||
useAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
this.automation.conditions.splice(index, 1);
|
||||
}
|
||||
},
|
||||
removeAction(index) {
|
||||
if (this.automation.actions.length <= 1) {
|
||||
useAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
|
||||
} else {
|
||||
this.automation.actions.splice(index, 1);
|
||||
}
|
||||
},
|
||||
submitAutomation() {
|
||||
// we assign it to this.errors so that it can be accessed in the template
|
||||
// it is supposed to be declared in the data function
|
||||
this.errors = validateAutomation(this.automation);
|
||||
if (Object.keys(this.errors).length === 0) {
|
||||
const automation = generateAutomationPayload(this.automation);
|
||||
this.$emit('saveAutomation', automation, this.mode);
|
||||
}
|
||||
},
|
||||
resetFilter(index, currentCondition) {
|
||||
this.automation.conditions[index].filter_operator = this.automationTypes[
|
||||
this.automation.event_name
|
||||
].conditions.find(
|
||||
condition => condition.key === currentCondition.attribute_key
|
||||
).filterOperators[0].value;
|
||||
this.automation.conditions[index].values = '';
|
||||
},
|
||||
showUserInput(type) {
|
||||
return !(type === 'is_present' || type === 'is_not_present');
|
||||
},
|
||||
showActionInput(action) {
|
||||
if (action === 'send_email_to_team' || action === 'send_message')
|
||||
return false;
|
||||
const type = this.automationActionTypes.find(
|
||||
i => i.key === action
|
||||
).inputType;
|
||||
return !!type;
|
||||
},
|
||||
resetAction(index) {
|
||||
this.automation.actions[index].action_params = [];
|
||||
},
|
||||
manifestConditions(automation) {
|
||||
const customAttributes = filterCustomAttributes(this.allCustomAttributes);
|
||||
const conditions = automation.conditions.map(condition => {
|
||||
const customAttr = isCustomAttribute(
|
||||
customAttributes,
|
||||
condition.attribute_key
|
||||
);
|
||||
let inputType = 'plain_text';
|
||||
if (customAttr) {
|
||||
inputType = getCustomAttributeInputType(customAttr.type);
|
||||
} else {
|
||||
inputType = getStandardAttributeInputType(
|
||||
this.automationTypes,
|
||||
automation.event_name,
|
||||
condition.attribute_key
|
||||
);
|
||||
}
|
||||
if (inputType === 'plain_text' || inputType === 'date') {
|
||||
return {
|
||||
...condition,
|
||||
values: condition.values[0],
|
||||
};
|
||||
}
|
||||
if (inputType === 'comma_separated_plain_text') {
|
||||
return {
|
||||
...condition,
|
||||
values: condition.values.join(','),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...condition,
|
||||
query_operator: condition.query_operator || 'and',
|
||||
values: [
|
||||
...this.getConditionDropdownValues(condition.attribute_key),
|
||||
].filter(item => [...condition.values].includes(item.id)),
|
||||
};
|
||||
});
|
||||
return conditions;
|
||||
},
|
||||
generateActionsArray(action) {
|
||||
const params = action.action_params;
|
||||
let actionParams = [];
|
||||
const inputType = this.automationActionTypes.find(
|
||||
item => item.key === action.action_name
|
||||
).inputType;
|
||||
if (inputType === 'multi_select' || inputType === 'search_select') {
|
||||
actionParams = [
|
||||
...this.getActionDropdownValues(action.action_name),
|
||||
].filter(item => [...params].includes(item.id));
|
||||
} else if (inputType === 'team_message') {
|
||||
actionParams = {
|
||||
team_ids: [
|
||||
...this.getActionDropdownValues(action.action_name),
|
||||
].filter(item => [...params[0].team_ids].includes(item.id)),
|
||||
message: params[0].message,
|
||||
};
|
||||
} else actionParams = [...params];
|
||||
return actionParams;
|
||||
},
|
||||
manifestActions(automation) {
|
||||
let actionParams = [];
|
||||
const actions = automation.actions.map(action => {
|
||||
if (action.action_params.length) {
|
||||
actionParams = this.generateActionsArray(action);
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
action_params: actionParams,
|
||||
};
|
||||
});
|
||||
return actions;
|
||||
},
|
||||
formatAutomation(automation) {
|
||||
this.automation = {
|
||||
...automation,
|
||||
conditions: this.manifestConditions(automation),
|
||||
actions: this.manifestActions(automation),
|
||||
};
|
||||
},
|
||||
getActionDropdownValues(type) {
|
||||
const { agents, labels, teams, slaPolicies } = this;
|
||||
return getActionOptions({
|
||||
agents,
|
||||
labels,
|
||||
teams,
|
||||
slaPolicies,
|
||||
languages,
|
||||
type,
|
||||
});
|
||||
},
|
||||
manifestCustomAttributes() {
|
||||
const conversationCustomAttributesRaw = this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
]('conversation_attribute');
|
||||
|
||||
const contactCustomAttributesRaw =
|
||||
this.$store.getters['attributes/getAttributesByModel'](
|
||||
'contact_attribute'
|
||||
);
|
||||
const conversationCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
conversationCustomAttributesRaw,
|
||||
'conversation_attribute'
|
||||
);
|
||||
const contactCustomAttributeTypes = generateCustomAttributeTypes(
|
||||
contactCustomAttributesRaw,
|
||||
'contact_attribute'
|
||||
);
|
||||
let manifestedCustomAttributes = generateCustomAttributes(
|
||||
conversationCustomAttributeTypes,
|
||||
contactCustomAttributeTypes,
|
||||
this.$t('AUTOMATION.CONDITION.CONVERSATION_CUSTOM_ATTR_LABEL'),
|
||||
this.$t('AUTOMATION.CONDITION.CONTACT_CUSTOM_ATTR_LABEL')
|
||||
);
|
||||
this.automationTypes.message_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
this.automationTypes.conversation_created.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
this.automationTypes.conversation_updated.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
this.automationTypes.conversation_opened.conditions.push(
|
||||
...manifestedCustomAttributes
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
accountSummaryKey: {
|
||||
type: String,
|
||||
default: 'getAccountSummary',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountReport: 'getAccountReports',
|
||||
}),
|
||||
accountSummary() {
|
||||
return this.$store.getters[this.accountSummaryKey];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
calculateTrend(key) {
|
||||
if (!this.accountSummary.previous[key]) return 0;
|
||||
const diff = this.accountSummary[key] - this.accountSummary.previous[key];
|
||||
return Math.round((diff / this.accountSummary.previous[key]) * 100);
|
||||
},
|
||||
displayMetric(key) {
|
||||
if (this.isAverageMetricType(key)) {
|
||||
return formatTime(this.accountSummary[key]);
|
||||
}
|
||||
return Number(this.accountSummary[key] || '').toLocaleString();
|
||||
},
|
||||
displayInfoText(key) {
|
||||
if (this.metrics[this.currentSelection].KEY !== key) {
|
||||
return '';
|
||||
}
|
||||
if (this.isAverageMetricType(key)) {
|
||||
const total = this.accountReport.data
|
||||
.map(item => item.count)
|
||||
.reduce((prev, curr) => prev + curr, 0);
|
||||
return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isAverageMetricType(key) {
|
||||
return [
|
||||
'avg_first_response_time',
|
||||
'avg_resolution_time',
|
||||
'reply_time',
|
||||
].includes(key);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import reportMixin from '../reportMixin';
|
||||
import reportFixtures from './reportMixinFixtures';
|
||||
import Vuex from 'vuex';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('reportMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
getAccountSummary: () => reportFixtures.summary,
|
||||
getBotSummary: () => reportFixtures.botSummary,
|
||||
getAccountReports: () => reportFixtures.report,
|
||||
};
|
||||
store = new Vuex.Store({ getters });
|
||||
});
|
||||
|
||||
it('display the metric for account', async () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
await wrapper.setProps({
|
||||
accountSummaryKey: 'getAccountSummary',
|
||||
});
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000');
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min 18 Sec'
|
||||
);
|
||||
});
|
||||
|
||||
it('display the metric for bot', async () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
await wrapper.setProps({
|
||||
accountSummaryKey: 'getBotSummary',
|
||||
});
|
||||
expect(wrapper.vm.displayMetric('bot_resolutions_count')).toEqual('10');
|
||||
expect(wrapper.vm.displayMetric('bot_handoffs_count')).toEqual('20');
|
||||
});
|
||||
|
||||
it('display the metric', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual('5,000');
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min 18 Sec'
|
||||
);
|
||||
});
|
||||
|
||||
it('calculate the trend', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(124900);
|
||||
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
||||
});
|
||||
|
||||
it('display info text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
currentSelection: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
DESC: '( Avg )',
|
||||
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||
KEY: 'avg_first_response_time',
|
||||
NAME: 'First Response Time',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayInfoText('avg_first_response_time')).toEqual(
|
||||
'Total number of conversations used for computation: 4'
|
||||
);
|
||||
});
|
||||
|
||||
it('do not display info text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
currentSelection: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
DESC: '( Total )',
|
||||
INFO_TEXT: '',
|
||||
KEY: 'conversation_count',
|
||||
NAME: 'Conversations',
|
||||
},
|
||||
{
|
||||
DESC: '( Avg )',
|
||||
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||
KEY: 'avg_first_response_time',
|
||||
NAME: 'First Response Time',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayInfoText('conversation_count')).toEqual('');
|
||||
expect(wrapper.vm.displayInfoText('incoming_messages_count')).toEqual('');
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
export default {
|
||||
summary: {
|
||||
avg_first_response_time: '198.6666666666667',
|
||||
avg_resolution_time: '208.3333333333333',
|
||||
conversations_count: 5000,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 3,
|
||||
previous: {
|
||||
avg_first_response_time: '89.0',
|
||||
avg_resolution_time: '145.0',
|
||||
conversations_count: 4,
|
||||
incoming_messages_count: 5,
|
||||
outgoing_messages_count: 4,
|
||||
resolutions_count: 0,
|
||||
},
|
||||
resolutions_count: 3,
|
||||
},
|
||||
botSummary: {
|
||||
bot_resolutions_count: 10,
|
||||
bot_handoffs_count: 20,
|
||||
previous: {
|
||||
bot_resolutions_count: 8,
|
||||
bot_handoffs_count: 5,
|
||||
},
|
||||
},
|
||||
report: {
|
||||
data: [
|
||||
{ value: '0.00', timestamp: 1647541800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647628200, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647714600, count: 0 },
|
||||
{ value: '0.00', timestamp: 1647801000, count: 0 },
|
||||
{ value: '0.01', timestamp: 1647887400, count: 4 },
|
||||
{ value: '0.00', timestamp: 1647973800, count: 0 },
|
||||
{ value: '0.00', timestamp: 1648060200, count: 0 },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { mapGetters } from 'vuex';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
|
||||
@@ -18,7 +18,6 @@ export default {
|
||||
TranslateModal,
|
||||
MenuItem,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -37,6 +36,12 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getPlainText } = useMessageFormatter();
|
||||
return {
|
||||
getPlainText,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCannedResponseModalOpen: false,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
mixins: [messageFormatterMixin],
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
@@ -28,6 +25,12 @@ export default {
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteModal: false,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import ReadMore from './ReadMore.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReadMore,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
author: {
|
||||
type: String,
|
||||
@@ -21,6 +20,13 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage, highlightContent } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
highlightContent,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOverflowing: false,
|
||||
|
||||
+14
-3
@@ -4,7 +4,7 @@ import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { filterAttributeGroups } from '../contactFilterItems';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import { useFilter } from 'shared/composables/useFilter';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
||||
@@ -13,7 +13,6 @@ export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -36,6 +35,15 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { setFilterAttributes } = useFilter({
|
||||
filteri18nKey: 'CONTACTS_FILTER',
|
||||
attributeModel: 'contact_attribute',
|
||||
});
|
||||
return {
|
||||
setFilterAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
@@ -69,7 +77,10 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
||||
this.filterGroups = filterGroups;
|
||||
|
||||
if (this.getAppliedContactFilters.length) {
|
||||
this.appliedFilters = [...this.getAppliedContactFilters];
|
||||
} else if (!this.isSegmentsView) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SwitchLayout from './SwitchLayout.vue';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
export default {
|
||||
@@ -14,7 +13,6 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
|
||||
+93
-17
@@ -1,9 +1,17 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import automationMethodsMixin from 'dashboard/mixins/automations/methodsMixin';
|
||||
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
|
||||
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
||||
|
||||
import { useAutomation } from 'dashboard/composables/useAutomation';
|
||||
import { validateAutomation } from 'dashboard/helper/validations';
|
||||
import {
|
||||
generateAutomationPayload,
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
import {
|
||||
AUTOMATION_RULE_EVENTS,
|
||||
AUTOMATION_ACTION_TYPES,
|
||||
@@ -14,13 +22,38 @@ export default {
|
||||
FilterInputBox,
|
||||
AutomationActionInput,
|
||||
},
|
||||
mixins: [automationMethodsMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
manifestCustomAttributes,
|
||||
} = useAutomation();
|
||||
return {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
manifestCustomAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
automationTypes: JSON.parse(JSON.stringify(AUTOMATIONS)),
|
||||
@@ -82,12 +115,24 @@ export default {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('campaigns/get');
|
||||
this.allCustomAttributes = this.$store.getters['attributes/getAttributes'];
|
||||
this.manifestCustomAttributes();
|
||||
this.manifestCustomAttributes(this.automationTypes);
|
||||
},
|
||||
methods: {
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
isFeatureEnabled(flag) {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, flag);
|
||||
},
|
||||
emitSaveAutomation() {
|
||||
this.errors = validateAutomation(this.automation);
|
||||
if (Object.keys(this.errors).length === 0) {
|
||||
const automation = generateAutomationPayload(this.automation);
|
||||
this.$emit('saveAutomation', automation, this.mode);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -121,7 +166,7 @@ export default {
|
||||
<select
|
||||
v-model="automation.event_name"
|
||||
class="m-0"
|
||||
@change="onEventChange()"
|
||||
@change="onEventChange(automation)"
|
||||
>
|
||||
<option
|
||||
v-for="event in automationRuleEvents"
|
||||
@@ -154,9 +199,26 @@ export default {
|
||||
v-for="(condition, i) in automation.conditions"
|
||||
:key="i"
|
||||
v-model="automation.conditions[i]"
|
||||
:filter-attributes="getAttributes(automation.event_name)"
|
||||
:input-type="getInputType(automation.conditions[i].attribute_key)"
|
||||
:operators="getOperators(automation.conditions[i].attribute_key)"
|
||||
:filter-attributes="
|
||||
getAttributes(automationTypes, automation.event_name)
|
||||
"
|
||||
:input-type="
|
||||
getInputType(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:operators="
|
||||
getOperators(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
mode,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:dropdown-values="
|
||||
getConditionDropdownValues(
|
||||
automation.conditions[i].attribute_key
|
||||
@@ -164,15 +226,26 @@ export default {
|
||||
"
|
||||
:show-query-operator="i !== automation.conditions.length - 1"
|
||||
:custom-attribute-type="
|
||||
getCustomAttributeType(automation.conditions[i].attribute_key)
|
||||
getCustomAttributeType(
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:error-message="
|
||||
errors[`condition_${i}`]
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`condition_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
@resetFilter="resetFilter(i, automation.conditions[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
@resetFilter="
|
||||
resetFilter(
|
||||
automation,
|
||||
automationTypes,
|
||||
i,
|
||||
automation.conditions[i]
|
||||
)
|
||||
"
|
||||
@removeFilter="removeFilter(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -180,7 +253,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewCondition"
|
||||
@click="appendNewCondition(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -205,15 +278,18 @@ export default {
|
||||
getActionDropdownValues(automation.actions[i].action_name)
|
||||
"
|
||||
:show-action-input="
|
||||
showActionInput(automation.actions[i].action_name)
|
||||
showActionInput(
|
||||
automationActionTypes,
|
||||
automation.actions[i].action_name
|
||||
)
|
||||
"
|
||||
:error-message="
|
||||
errors[`action_${i}`]
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`action_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
@resetAction="resetAction(i)"
|
||||
@removeAction="removeAction(i)"
|
||||
@resetAction="resetAction(automation, i)"
|
||||
@removeAction="removeAction(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -221,7 +297,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewAction"
|
||||
@click="appendNewAction(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.ACTION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -234,7 +310,7 @@ export default {
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('AUTOMATION.ADD.CANCEL_BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
<woot-button @click="submitAutomation">
|
||||
<woot-button @click="emitSaveAutomation">
|
||||
{{ $t('AUTOMATION.ADD.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
+106
-17
@@ -1,8 +1,18 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import automationMethodsMixin from 'dashboard/mixins/automations/methodsMixin';
|
||||
import { useAutomation } from 'dashboard/composables/useAutomation';
|
||||
import FilterInputBox from 'dashboard/components/widgets/FilterInput/Index.vue';
|
||||
import AutomationActionInput from 'dashboard/components/widgets/AutomationActionInput.vue';
|
||||
import {
|
||||
getFileName,
|
||||
generateAutomationPayload,
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
} from 'dashboard/helper/automationHelper';
|
||||
import { validateAutomation } from 'dashboard/helper/validations';
|
||||
|
||||
import {
|
||||
AUTOMATION_RULE_EVENTS,
|
||||
@@ -15,7 +25,6 @@ export default {
|
||||
FilterInputBox,
|
||||
AutomationActionInput,
|
||||
},
|
||||
mixins: [automationMethodsMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -26,6 +35,34 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
formatAutomation,
|
||||
manifestCustomAttributes,
|
||||
} = useAutomation();
|
||||
return {
|
||||
onEventChange,
|
||||
getConditionDropdownValues,
|
||||
appendNewCondition,
|
||||
appendNewAction,
|
||||
removeFilter,
|
||||
removeAction,
|
||||
resetFilter,
|
||||
resetAction,
|
||||
getActionDropdownValues,
|
||||
formatAutomation,
|
||||
manifestCustomAttributes,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
automationTypes: JSON.parse(JSON.stringify(AUTOMATIONS)),
|
||||
@@ -61,14 +98,33 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.manifestCustomAttributes();
|
||||
this.manifestCustomAttributes(this.automationTypes);
|
||||
this.allCustomAttributes = this.$store.getters['attributes/getAttributes'];
|
||||
this.formatAutomation(this.selectedResponse);
|
||||
|
||||
this.automation = this.formatAutomation(
|
||||
this.selectedResponse,
|
||||
this.allCustomAttributes,
|
||||
this.automationTypes,
|
||||
this.automationActionTypes
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
getFileName,
|
||||
getAttributes,
|
||||
getInputType,
|
||||
getOperators,
|
||||
getCustomAttributeType,
|
||||
showActionInput,
|
||||
isFeatureEnabled(flag) {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, flag);
|
||||
},
|
||||
emitSaveAutomation() {
|
||||
this.errors = validateAutomation(this.automation);
|
||||
if (Object.keys(this.errors).length === 0) {
|
||||
const automation = generateAutomationPayload(this.automation);
|
||||
this.$emit('saveAutomation', automation, this.mode);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -99,7 +155,10 @@ export default {
|
||||
<div class="event_wrapper">
|
||||
<label :class="{ error: errors.event_name }">
|
||||
{{ $t('AUTOMATION.ADD.FORM.EVENT.LABEL') }}
|
||||
<select v-model="automation.event_name" @change="onEventChange()">
|
||||
<select
|
||||
v-model="automation.event_name"
|
||||
@change="onEventChange(automation)"
|
||||
>
|
||||
<option
|
||||
v-for="event in automationRuleEvents"
|
||||
:key="event.key"
|
||||
@@ -125,16 +184,37 @@ export default {
|
||||
v-for="(condition, i) in automation.conditions"
|
||||
:key="i"
|
||||
v-model="automation.conditions[i]"
|
||||
:filter-attributes="getAttributes(automation.event_name)"
|
||||
:input-type="getInputType(automation.conditions[i].attribute_key)"
|
||||
:operators="getOperators(automation.conditions[i].attribute_key)"
|
||||
:filter-attributes="
|
||||
getAttributes(automationTypes, automation.event_name)
|
||||
"
|
||||
:input-type="
|
||||
getInputType(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:operators="
|
||||
getOperators(
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
mode,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:dropdown-values="
|
||||
getConditionDropdownValues(
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:custom-attribute-type="
|
||||
getCustomAttributeType(automation.conditions[i].attribute_key)
|
||||
getCustomAttributeType(
|
||||
automationTypes,
|
||||
automation,
|
||||
automation.conditions[i].attribute_key
|
||||
)
|
||||
"
|
||||
:show-query-operator="i !== automation.conditions.length - 1"
|
||||
:error-message="
|
||||
@@ -142,8 +222,15 @@ export default {
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`condition_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
@resetFilter="resetFilter(i, automation.conditions[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
@resetFilter="
|
||||
resetFilter(
|
||||
automation,
|
||||
automationTypes,
|
||||
i,
|
||||
automation.conditions[i]
|
||||
)
|
||||
"
|
||||
@removeFilter="removeFilter(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -151,7 +238,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewCondition"
|
||||
@click="appendNewCondition(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.CONDITION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -173,15 +260,17 @@ export default {
|
||||
v-model="automation.actions[i]"
|
||||
:action-types="automationActionTypes"
|
||||
:dropdown-values="getActionDropdownValues(action.action_name)"
|
||||
:show-action-input="showActionInput(action.action_name)"
|
||||
:show-action-input="
|
||||
showActionInput(automationActionTypes, action.action_name)
|
||||
"
|
||||
:error-message="
|
||||
errors[`action_${i}`]
|
||||
? $t(`AUTOMATION.ERRORS.${errors[`action_${i}`]}`)
|
||||
: ''
|
||||
"
|
||||
:initial-file-name="getFileName(action, automation.files)"
|
||||
@resetAction="resetAction(i)"
|
||||
@removeAction="removeAction(i)"
|
||||
@resetAction="resetAction(automation, i)"
|
||||
@removeAction="removeAction(automation, i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
@@ -189,7 +278,7 @@ export default {
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewAction"
|
||||
@click="appendNewAction(automation)"
|
||||
>
|
||||
{{ $t('AUTOMATION.ADD.ACTION_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
@@ -206,7 +295,7 @@ export default {
|
||||
>
|
||||
{{ $t('AUTOMATION.EDIT.CANCEL_BUTTON_TEXT') }}
|
||||
</woot-button>
|
||||
<woot-button @click="submitAutomation">
|
||||
<woot-button @click="emitSaveAutomation">
|
||||
{{ $t('AUTOMATION.EDIT.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
@@ -7,12 +7,12 @@ import BillingItem from './components/BillingItem.vue';
|
||||
|
||||
export default {
|
||||
components: { BillingItem },
|
||||
mixins: [messageFormatterMixin],
|
||||
setup() {
|
||||
const { accountId } = useAccount();
|
||||
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
accountId,
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import WootDateTimePicker from 'dashboard/components/ui/DateTimePicker.vue';
|
||||
import { URLPattern } from 'urlpattern-polyfill';
|
||||
import { CAMPAIGNS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
@@ -14,9 +14,9 @@ export default {
|
||||
WootDateTimePicker,
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
const { campaignType, isOngoingType, isOneOffType } = useCampaign();
|
||||
return { v$: useVuelidate(), campaignType, isOngoingType, isOneOffType };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import CampaignsTable from './CampaignsTable.vue';
|
||||
import EditCampaign from './EditCampaign.vue';
|
||||
export default {
|
||||
@@ -9,13 +9,16 @@ export default {
|
||||
CampaignsTable,
|
||||
EditCampaign,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { campaignType } = useCampaign();
|
||||
return { campaignType };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showEditPopup: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
|
||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { messageStamp } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
@@ -9,7 +9,6 @@ export default {
|
||||
UserAvatarWithName,
|
||||
InboxName,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
campaign: {
|
||||
type: Object,
|
||||
@@ -20,7 +19,12 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
campaignStatus() {
|
||||
if (this.isOngoingType) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import CampaignCard from './CampaignCard.vue';
|
||||
|
||||
export default {
|
||||
@@ -10,9 +10,6 @@ export default {
|
||||
Spinner,
|
||||
CampaignCard,
|
||||
},
|
||||
|
||||
mixins: [campaignMixin],
|
||||
|
||||
props: {
|
||||
campaigns: {
|
||||
type: Array,
|
||||
@@ -27,7 +24,10 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { isOngoingType } = useCampaign();
|
||||
return { isOngoingType };
|
||||
},
|
||||
computed: {
|
||||
currentInboxId() {
|
||||
return this.$route.params.inboxId;
|
||||
|
||||
@@ -4,14 +4,13 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import { URLPattern } from 'urlpattern-polyfill';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
props: {
|
||||
selectedCampaign: {
|
||||
type: Object,
|
||||
@@ -19,7 +18,8 @@ export default {
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
const { isOngoingType } = useCampaign();
|
||||
return { v$: useVuelidate(), isOngoingType };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import campaignMixin from 'shared/mixins/campaignMixin';
|
||||
import { useCampaign } from 'shared/composables/useCampaign';
|
||||
import Campaign from './Campaign.vue';
|
||||
import AddCampaign from './AddCampaign.vue';
|
||||
|
||||
@@ -8,7 +8,10 @@ export default {
|
||||
Campaign,
|
||||
AddCampaign,
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
setup() {
|
||||
const { isOngoingType } = useCampaign();
|
||||
return { isOngoingType };
|
||||
},
|
||||
data() {
|
||||
return { showAddPopup: false };
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Integration from './Integration.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
|
||||
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
@@ -13,7 +12,7 @@ export default {
|
||||
SelectChannelWarning,
|
||||
SlackIntegrationHelpText,
|
||||
},
|
||||
mixins: [globalConfigMixin, messageFormatterMixin],
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
code: { type: String, default: '' },
|
||||
},
|
||||
|
||||
+8
-2
@@ -2,16 +2,22 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin, messageFormatterMixin],
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
hasConnectedAChannel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { selectedChannelId: '', availableChannels: [] };
|
||||
},
|
||||
|
||||
+7
-2
@@ -1,13 +1,18 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
selectedChannelName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAlert } from 'dashboard/composables';
|
||||
import BotMetrics from './components/BotMetrics.vue';
|
||||
import ReportFilterSelector from './components/FilterSelector.vue';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import ReportContainer from './ReportContainer.vue';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -14,7 +13,6 @@ export default {
|
||||
ReportFilterSelector,
|
||||
ReportContainer,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
|
||||
@@ -4,7 +4,6 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import ReportFilterSelector from './components/FilterSelector.vue';
|
||||
import { GROUP_BY_FILTER } from './constants';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import ReportContainer from './ReportContainer.vue';
|
||||
|
||||
@@ -24,7 +23,6 @@ export default {
|
||||
ReportFilterSelector,
|
||||
ReportContainer,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useReportMetrics } from 'dashboard/composables/useReportMetrics';
|
||||
import { GROUP_BY_FILTER, METRIC_CHART } from './constants';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import format from 'date-fns/format';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
import ChartStats from './components/ChartElements/ChartStats.vue';
|
||||
|
||||
export default {
|
||||
components: { ChartStats },
|
||||
mixins: [reportMixin],
|
||||
props: {
|
||||
groupBy: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
accountSummaryKey: {
|
||||
type: String,
|
||||
default: 'getAccountSummary',
|
||||
},
|
||||
reportKeys: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
@@ -27,7 +31,16 @@ export default {
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { calculateTrend, isAverageMetricType } = useReportMetrics(
|
||||
props.accountSummaryKey
|
||||
);
|
||||
return { calculateTrend, isAverageMetricType };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountReport: 'getAccountReports',
|
||||
}),
|
||||
metrics() {
|
||||
const reportKeys = Object.keys(this.reportKeys);
|
||||
const infoText = {
|
||||
@@ -121,12 +134,12 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 bg-white dark:bg-slate-800 p-2 border border-slate-100 dark:border-slate-700 rounded-md"
|
||||
class="grid grid-cols-1 p-2 bg-white border rounded-md md:grid-cols-2 lg:grid-cols-3 dark:bg-slate-800 border-slate-100 dark:border-slate-700"
|
||||
>
|
||||
<div
|
||||
v-for="metric in metrics"
|
||||
:key="metric.KEY"
|
||||
class="p-4 rounded-md mb-3"
|
||||
class="p-4 mb-3 rounded-md"
|
||||
>
|
||||
<ChartStats :metric="metric" :account-summary-key="accountSummaryKey" />
|
||||
<div class="mt-4 h-72">
|
||||
@@ -135,12 +148,12 @@ export default {
|
||||
class="text-xs"
|
||||
:message="$t('REPORT.LOADING_CHART')"
|
||||
/>
|
||||
<div v-else class="h-72 flex items-center justify-center">
|
||||
<div v-else class="flex items-center justify-center h-72">
|
||||
<woot-bar
|
||||
v-if="accountReport.data[metric.KEY].length"
|
||||
:collection="getCollection(metric)"
|
||||
:chart-options="getChartOptions(metric)"
|
||||
class="h-72 w-full"
|
||||
class="w-full h-72"
|
||||
/>
|
||||
<span v-else class="text-sm text-slate-600">
|
||||
{{ $t('REPORT.NO_ENOUGH_DATA') }}
|
||||
|
||||
+26
-21
@@ -1,25 +1,30 @@
|
||||
<script>
|
||||
import reportMixin from 'dashboard/mixins/reportMixin';
|
||||
export default {
|
||||
mixins: [reportMixin],
|
||||
props: {
|
||||
metric: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
<script setup>
|
||||
import { useReportMetrics } from 'dashboard/composables/useReportMetrics';
|
||||
|
||||
const props = defineProps({
|
||||
metric: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
methods: {
|
||||
trendColor(value, key) {
|
||||
if (this.isAverageMetricType(key)) {
|
||||
return value > 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
}
|
||||
return value < 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
},
|
||||
accountSummaryKey: {
|
||||
type: String,
|
||||
default: 'getAccountSummary',
|
||||
},
|
||||
});
|
||||
|
||||
const { calculateTrend, displayMetric, isAverageMetricType } = useReportMetrics(
|
||||
props.accountSummaryKey
|
||||
);
|
||||
|
||||
const trendColor = (value, key) => {
|
||||
if (isAverageMetricType(key)) {
|
||||
return value > 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
}
|
||||
return value < 0
|
||||
? 'border-red-500 text-red-500'
|
||||
: 'border-green-500 text-green-500';
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -29,7 +34,7 @@ export default {
|
||||
{{ metric.NAME }}
|
||||
</span>
|
||||
<div class="flex items-end">
|
||||
<div class="font-medium text-xl">
|
||||
<div class="text-xl font-medium">
|
||||
{{ displayMetric(metric.KEY) }}
|
||||
</div>
|
||||
<div v-if="metric.trend" class="text-xs ml-4 flex items-center mb-0.5">
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAlert } from 'dashboard/composables';
|
||||
import ReportFilters from './ReportFilters.vue';
|
||||
import ReportContainer from '../ReportContainer.vue';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
import reportMixin from '../../../../../mixins/reportMixin';
|
||||
import { generateFileName } from '../../../../../helper/downloadHelper';
|
||||
import { REPORTS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -22,7 +21,6 @@ export default {
|
||||
ReportFilters,
|
||||
ReportContainer,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script>
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -30,7 +29,7 @@ export default {
|
||||
setup(props) {
|
||||
const selectedIndex = ref(-1);
|
||||
const portalSearchSuggestionsRef = ref(null);
|
||||
|
||||
const { highlightContent } = useMessageFormatter();
|
||||
const adjustScroll = () => {
|
||||
nextTick(() => {
|
||||
portalSearchSuggestionsRef.value.scrollTop = 102 * selectedIndex.value;
|
||||
@@ -53,6 +52,7 @@ export default {
|
||||
selectedIndex,
|
||||
portalSearchSuggestionsRef,
|
||||
isSearchItemActive,
|
||||
highlightContent,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import ChatOption from 'shared/components/ChatOption.vue';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatOption,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -25,6 +24,12 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
isSelected(option) {
|
||||
return this.selected === option.id;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useCampaign } from '../useCampaign';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import { CAMPAIGN_TYPES } from '../../constants/campaign';
|
||||
|
||||
// Mock the useRoute composable
|
||||
vi.mock('dashboard/composables/route', () => ({
|
||||
useRoute: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useCampaign', () => {
|
||||
it('returns the correct campaign type for ongoing campaigns', () => {
|
||||
useRoute.mockReturnValue({ name: 'ongoing_campaigns' });
|
||||
const { campaignType } = useCampaign();
|
||||
expect(campaignType.value).toBe(CAMPAIGN_TYPES.ONGOING);
|
||||
});
|
||||
|
||||
it('returns the correct campaign type for one-off campaigns', () => {
|
||||
useRoute.mockReturnValue({ name: 'one_off' });
|
||||
const { campaignType } = useCampaign();
|
||||
expect(campaignType.value).toBe(CAMPAIGN_TYPES.ONE_OFF);
|
||||
});
|
||||
|
||||
it('isOngoingType returns true for ongoing campaigns', () => {
|
||||
useRoute.mockReturnValue({ name: 'ongoing_campaigns' });
|
||||
const { isOngoingType } = useCampaign();
|
||||
expect(isOngoingType.value).toBe(true);
|
||||
});
|
||||
|
||||
it('isOngoingType returns false for one-off campaigns', () => {
|
||||
useRoute.mockReturnValue({ name: 'one_off' });
|
||||
const { isOngoingType } = useCampaign();
|
||||
expect(isOngoingType.value).toBe(false);
|
||||
});
|
||||
|
||||
it('isOneOffType returns true for one-off campaigns', () => {
|
||||
useRoute.mockReturnValue({ name: 'one_off' });
|
||||
const { isOneOffType } = useCampaign();
|
||||
expect(isOneOffType.value).toBe(true);
|
||||
});
|
||||
|
||||
it('isOneOffType returns false for ongoing campaigns', () => {
|
||||
useRoute.mockReturnValue({ name: 'ongoing_campaigns' });
|
||||
const { isOneOffType } = useCampaign();
|
||||
expect(isOneOffType.value).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useFilter } from '../useFilter';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('dashboard/composables/useI18n');
|
||||
|
||||
describe('useFilter', () => {
|
||||
// Setup mocks
|
||||
beforeEach(() => {
|
||||
vi.mocked(useStore).mockReturnValue({
|
||||
getters: {
|
||||
'attributes/getAttributesByModel': vi.fn(),
|
||||
},
|
||||
});
|
||||
vi.mocked(useI18n).mockReturnValue({
|
||||
t: vi.fn(key => key),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct functions', () => {
|
||||
const {
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
} = useFilter({ filteri18nKey: 'TEST', attributeModel: 'conversation' });
|
||||
|
||||
expect(setFilterAttributes).toBeDefined();
|
||||
expect(initializeStatusAndAssigneeFilterToModal).toBeDefined();
|
||||
expect(initializeInboxTeamAndLabelFilterToModal).toBeDefined();
|
||||
});
|
||||
|
||||
describe('setFilterAttributes', () => {
|
||||
it('should return filterGroups and filterTypes', () => {
|
||||
const mockAttributes = [
|
||||
{
|
||||
attribute_key: 'test_key',
|
||||
attribute_display_name: 'Test Name',
|
||||
attribute_display_type: 'text',
|
||||
},
|
||||
];
|
||||
vi.mocked(useStore)().getters[
|
||||
'attributes/getAttributesByModel'
|
||||
].mockReturnValue(mockAttributes);
|
||||
|
||||
const { setFilterAttributes } = useFilter({
|
||||
filteri18nKey: 'TEST',
|
||||
attributeModel: 'conversation',
|
||||
});
|
||||
const result = setFilterAttributes();
|
||||
|
||||
expect(result).toHaveProperty('filterGroups');
|
||||
expect(result).toHaveProperty('filterTypes');
|
||||
expect(result.filterGroups.length).toBeGreaterThan(0);
|
||||
expect(result.filterTypes.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeStatusAndAssigneeFilterToModal', () => {
|
||||
it('should return status filter when activeStatus is provided', () => {
|
||||
const { initializeStatusAndAssigneeFilterToModal } = useFilter({
|
||||
filteri18nKey: 'TEST',
|
||||
attributeModel: 'conversation',
|
||||
});
|
||||
const result = initializeStatusAndAssigneeFilterToModal('open', {}, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
attribute_key: 'status',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{ id: 'open', name: 'CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT' },
|
||||
],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null when no active filters', () => {
|
||||
const { initializeStatusAndAssigneeFilterToModal } = useFilter({
|
||||
filteri18nKey: 'TEST',
|
||||
attributeModel: 'conversation',
|
||||
});
|
||||
const result = initializeStatusAndAssigneeFilterToModal('', {}, '');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeInboxTeamAndLabelFilterToModal', () => {
|
||||
it('should return filters for inbox, team, and label when provided', () => {
|
||||
const { initializeInboxTeamAndLabelFilterToModal } = useFilter({
|
||||
filteri18nKey: 'TEST',
|
||||
attributeModel: 'conversation',
|
||||
});
|
||||
const result = initializeInboxTeamAndLabelFilterToModal(
|
||||
1,
|
||||
{ name: 'Inbox 1' },
|
||||
2,
|
||||
[{ id: 2, name: 'Team 2' }],
|
||||
'Label 1'
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0]).toHaveProperty('attribute_key', 'inbox_id');
|
||||
expect(result[1]).toHaveProperty('attribute_key', 'team_id');
|
||||
expect(result[2]).toHaveProperty('attribute_key', 'labels');
|
||||
});
|
||||
|
||||
it('should return empty array when no filters are provided', () => {
|
||||
const { initializeInboxTeamAndLabelFilterToModal } = useFilter({
|
||||
filteri18nKey: 'TEST',
|
||||
attributeModel: 'conversation',
|
||||
});
|
||||
const result = initializeInboxTeamAndLabelFilterToModal(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return only inbox filter when only inbox is provided', () => {
|
||||
const { initializeInboxTeamAndLabelFilterToModal } = useFilter({
|
||||
filteri18nKey: 'TEST',
|
||||
attributeModel: 'conversation',
|
||||
});
|
||||
const result = initializeInboxTeamAndLabelFilterToModal(
|
||||
1,
|
||||
{ name: 'Inbox 1' },
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveProperty('attribute_key', 'inbox_id');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useMessageFormatter } from '../useMessageFormatter';
|
||||
|
||||
describe('useMessageFormatter', () => {
|
||||
let messageFormatter;
|
||||
|
||||
beforeEach(() => {
|
||||
messageFormatter = useMessageFormatter();
|
||||
});
|
||||
|
||||
describe('formatMessage', () => {
|
||||
it('should format a regular message correctly', () => {
|
||||
const message = 'This is a [test](https://example.com) message';
|
||||
const result = messageFormatter.formatMessage(message, false, false);
|
||||
expect(result).toContain('<a href="https://example.com"');
|
||||
expect(result).toContain('class="link"');
|
||||
});
|
||||
|
||||
it('should format a tweet correctly', () => {
|
||||
const message = '@user #hashtag';
|
||||
const result = messageFormatter.formatMessage(message, true, false);
|
||||
expect(result).toContain('<a href="http://twitter.com/user"');
|
||||
expect(result).toContain('<a href="https://twitter.com/hashtag/hashtag"');
|
||||
});
|
||||
|
||||
it('should not format mentions and hashtags for private notes', () => {
|
||||
const message = '@user #hashtag';
|
||||
const result = messageFormatter.formatMessage(message, false, true);
|
||||
expect(result).not.toContain('<a href="http://twitter.com/user"');
|
||||
expect(result).not.toContain(
|
||||
'<a href="https://twitter.com/hashtag/hashtag"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncateMessage', () => {
|
||||
it('should not truncate short messages', () => {
|
||||
const message = 'Short message';
|
||||
const result = messageFormatter.truncateMessage(message);
|
||||
expect(result).toBe(message);
|
||||
});
|
||||
|
||||
it('should truncate long messages', () => {
|
||||
const message = 'A'.repeat(150);
|
||||
const result = messageFormatter.truncateMessage(message);
|
||||
expect(result.length).toBe(100);
|
||||
expect(result.endsWith('...')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('highlightContent', () => {
|
||||
it('should highlight search term in content', () => {
|
||||
const content = 'This is a test message';
|
||||
const searchTerm = 'test';
|
||||
const highlightClass = 'highlight';
|
||||
const result = messageFormatter.highlightContent(
|
||||
content,
|
||||
searchTerm,
|
||||
highlightClass
|
||||
);
|
||||
expect(result.trim()).toBe(
|
||||
'This is a <span class="highlight">test</span> message'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle special characters in search term', () => {
|
||||
const content = 'This (message) contains [special] characters';
|
||||
const searchTerm = '(message)';
|
||||
const highlightClass = 'highlight';
|
||||
const result = messageFormatter.highlightContent(
|
||||
content,
|
||||
searchTerm,
|
||||
highlightClass
|
||||
);
|
||||
expect(result.trim()).toBe(
|
||||
'This <span class="highlight">(message)</span> contains [special] characters'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import { CAMPAIGN_TYPES } from '../constants/campaign';
|
||||
|
||||
/**
|
||||
* Composable to manage campaign types.
|
||||
*
|
||||
* @returns {Object} - Computed properties for campaign type and its checks.
|
||||
*/
|
||||
export const useCampaign = () => {
|
||||
const route = useRoute();
|
||||
|
||||
/**
|
||||
* Computed property to determine the current campaign type based on the route name.
|
||||
*/
|
||||
const campaignType = computed(() => {
|
||||
const campaignTypeMap = {
|
||||
ongoing_campaigns: CAMPAIGN_TYPES.ONGOING,
|
||||
one_off: CAMPAIGN_TYPES.ONE_OFF,
|
||||
};
|
||||
return campaignTypeMap[route.name];
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed property to check if the current campaign type is 'ongoing'.
|
||||
*/
|
||||
const isOngoingType = computed(() => {
|
||||
return campaignType.value === CAMPAIGN_TYPES.ONGOING;
|
||||
});
|
||||
|
||||
/**
|
||||
* Computed property to check if the current campaign type is 'one-off'.
|
||||
*/
|
||||
const isOneOffType = computed(() => {
|
||||
return campaignType.value === CAMPAIGN_TYPES.ONE_OFF;
|
||||
});
|
||||
|
||||
return {
|
||||
campaignType,
|
||||
isOngoingType,
|
||||
isOneOffType,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { filterAttributeGroups } from 'dashboard/components/widgets/conversation/advancedFilterItems';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
|
||||
const customAttributeInputType = key => {
|
||||
switch (key) {
|
||||
case 'date':
|
||||
return 'date';
|
||||
case 'text':
|
||||
return 'plain_text';
|
||||
case 'list':
|
||||
return 'search_select';
|
||||
case 'checkbox':
|
||||
return 'search_select';
|
||||
default:
|
||||
return 'plain_text';
|
||||
}
|
||||
};
|
||||
|
||||
const getOperatorTypes = key => {
|
||||
switch (key) {
|
||||
case 'list':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'text':
|
||||
return OPERATORS.OPERATOR_TYPES_3;
|
||||
case 'number':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'link':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
case 'date':
|
||||
return OPERATORS.OPERATOR_TYPES_4;
|
||||
case 'checkbox':
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
default:
|
||||
return OPERATORS.OPERATOR_TYPES_1;
|
||||
}
|
||||
};
|
||||
|
||||
export const useFilter = ({ filteri18nKey, attributeModel }) => {
|
||||
const { t: $t } = useI18n();
|
||||
const { getters } = useStore();
|
||||
|
||||
const setFilterAttributes = () => {
|
||||
const allCustomAttributes =
|
||||
getters['attributes/getAttributesByModel'](attributeModel);
|
||||
|
||||
const customAttributesFormatted = {
|
||||
name: $t(`${filteri18nKey}.GROUPS.CUSTOM_ATTRIBUTES`),
|
||||
attributes: allCustomAttributes.map(attr => {
|
||||
return {
|
||||
key: attr.attribute_key,
|
||||
name: attr.attribute_display_name,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const allFilterGroups = filterAttributeGroups.map(group => {
|
||||
return {
|
||||
name: $t(`${filteri18nKey}.GROUPS.${group.i18nGroup}`),
|
||||
attributes: group.attributes.map(attribute => {
|
||||
return {
|
||||
key: attribute.key,
|
||||
name: $t(`${filteri18nKey}.ATTRIBUTES.${attribute.i18nKey}`),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const customAttributeTypes = allCustomAttributes.map(attr => {
|
||||
return {
|
||||
attributeKey: attr.attribute_key,
|
||||
attributeI18nKey: `CUSTOM_ATTRIBUTE_${attr.attribute_display_type.toUpperCase()}`,
|
||||
inputType: customAttributeInputType(attr.attribute_display_type),
|
||||
filterOperators: getOperatorTypes(attr.attribute_display_type),
|
||||
attributeModel: 'custom_attributes',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
filterGroups: [...allFilterGroups, customAttributesFormatted],
|
||||
filterTypes: [...customAttributeTypes],
|
||||
};
|
||||
};
|
||||
|
||||
const initializeStatusAndAssigneeFilterToModal = (
|
||||
activeStatus,
|
||||
currentUserDetails,
|
||||
activeAssigneeTab
|
||||
) => {
|
||||
if (activeStatus !== '') {
|
||||
return {
|
||||
attribute_key: 'status',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{
|
||||
id: activeStatus,
|
||||
name: $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`),
|
||||
},
|
||||
],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
};
|
||||
}
|
||||
if (activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ME) {
|
||||
return {
|
||||
attribute_key: 'assignee_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: currentUserDetails,
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const initializeInboxTeamAndLabelFilterToModal = (
|
||||
conversationInbox,
|
||||
inbox,
|
||||
teamId,
|
||||
activeTeam,
|
||||
label
|
||||
) => {
|
||||
const filters = [];
|
||||
if (conversationInbox) {
|
||||
filters.push({
|
||||
attribute_key: 'inbox_id',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{
|
||||
id: conversationInbox,
|
||||
name: inbox.name,
|
||||
},
|
||||
],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
if (teamId) {
|
||||
filters.push({
|
||||
attribute_key: 'team_id',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: activeTeam,
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
if (label) {
|
||||
filters.push({
|
||||
attribute_key: 'labels',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{
|
||||
id: label,
|
||||
name: label,
|
||||
},
|
||||
],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
return filters;
|
||||
};
|
||||
|
||||
return {
|
||||
setFilterAttributes,
|
||||
initializeStatusAndAssigneeFilterToModal,
|
||||
initializeInboxTeamAndLabelFilterToModal,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import MessageFormatter from '../helpers/MessageFormatter';
|
||||
|
||||
/**
|
||||
* A composable providing utility functions for message formatting.
|
||||
*
|
||||
* @returns {Object} A set of functions for message formatting.
|
||||
*/
|
||||
export const useMessageFormatter = () => {
|
||||
/**
|
||||
* Formats a message based on specified conditions.
|
||||
*
|
||||
* @param {string} message - The message to be formatted.
|
||||
* @param {boolean} isATweet - Whether the message is a tweet.
|
||||
* @param {boolean} isAPrivateNote - Whether the message is a private note.
|
||||
* @returns {string} - The formatted message.
|
||||
*/
|
||||
const formatMessage = (message, isATweet, isAPrivateNote) => {
|
||||
const messageFormatter = new MessageFormatter(
|
||||
message,
|
||||
isATweet,
|
||||
isAPrivateNote
|
||||
);
|
||||
return messageFormatter.formattedMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a message to plain text.
|
||||
*
|
||||
* @param {string} message - The message to be converted.
|
||||
* @param {boolean} isATweet - Whether the message is a tweet.
|
||||
* @returns {string} - The plain text message.
|
||||
*/
|
||||
const getPlainText = (message, isATweet) => {
|
||||
const messageFormatter = new MessageFormatter(message, isATweet);
|
||||
return messageFormatter.plainText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncates a description to a maximum length of 100 characters.
|
||||
*
|
||||
* @param {string} [description=''] - The description to be truncated.
|
||||
* @returns {string} - The truncated description.
|
||||
*/
|
||||
const truncateMessage = (description = '') => {
|
||||
if (description.length < 100) {
|
||||
return description;
|
||||
}
|
||||
|
||||
return `${description.slice(0, 97)}...`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlights occurrences of a search term within given content.
|
||||
*
|
||||
* @param {string} [content=''] - The content in which to search.
|
||||
* @param {string} [searchTerm=''] - The term to search for.
|
||||
* @param {string} [highlightClass=''] - The CSS class to apply to the highlighted term.
|
||||
* @returns {string} - The content with highlighted terms.
|
||||
*/
|
||||
const highlightContent = (
|
||||
content = '',
|
||||
searchTerm = '',
|
||||
highlightClass = ''
|
||||
) => {
|
||||
const plainTextContent = getPlainText(content);
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
return plainTextContent.replace(
|
||||
new RegExp(`(${escapedSearchTerm})`, 'ig'),
|
||||
`<span class="${highlightClass}">$1</span>`
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
formatMessage,
|
||||
getPlainText,
|
||||
truncateMessage,
|
||||
highlightContent,
|
||||
};
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { CAMPAIGN_TYPES } from '../constants/campaign';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
campaignType() {
|
||||
const campaignTypeMap = {
|
||||
ongoing_campaigns: CAMPAIGN_TYPES.ONGOING,
|
||||
one_off: CAMPAIGN_TYPES.ONE_OFF,
|
||||
};
|
||||
return campaignTypeMap[this.$route.name];
|
||||
},
|
||||
isOngoingType() {
|
||||
return this.campaignType === CAMPAIGN_TYPES.ONGOING;
|
||||
},
|
||||
isOneOffType() {
|
||||
return this.campaignType === CAMPAIGN_TYPES.ONE_OFF;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
setFilterAttributes() {
|
||||
const allCustomAttributes = this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
](this.attributeModel);
|
||||
const customAttributesFormatted = {
|
||||
name: this.$t(`${this.filtersFori18n}.GROUPS.CUSTOM_ATTRIBUTES`),
|
||||
attributes: allCustomAttributes.map(attr => {
|
||||
return {
|
||||
key: attr.attribute_key,
|
||||
name: attr.attribute_display_name,
|
||||
};
|
||||
}),
|
||||
};
|
||||
const allFilterGroups = this.filterAttributeGroups.map(group => {
|
||||
return {
|
||||
name: this.$t(`${this.filtersFori18n}.GROUPS.${group.i18nGroup}`),
|
||||
attributes: group.attributes.map(attribute => {
|
||||
return {
|
||||
key: attribute.key,
|
||||
name: this.$t(
|
||||
`${this.filtersFori18n}.ATTRIBUTES.${attribute.i18nKey}`
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
const customAttributeTypes = allCustomAttributes.map(attr => {
|
||||
return {
|
||||
attributeKey: attr.attribute_key,
|
||||
attributeI18nKey: `CUSTOM_ATTRIBUTE_${attr.attribute_display_type.toUpperCase()}`,
|
||||
inputType: this.customAttributeInputType(attr.attribute_display_type),
|
||||
filterOperators: this.getOperatorTypes(attr.attribute_display_type),
|
||||
attributeModel: 'custom_attributes',
|
||||
};
|
||||
});
|
||||
this.filterTypes = [...this.filterTypes, ...customAttributeTypes];
|
||||
this.filterGroups = [...allFilterGroups, customAttributesFormatted];
|
||||
},
|
||||
|
||||
initializeExistingFilterToModal() {
|
||||
this.initializeStatusAndAssigneeFilterToModal();
|
||||
this.initializeInboxTeamAndLabelFilterToModal();
|
||||
},
|
||||
initializeStatusAndAssigneeFilterToModal() {
|
||||
if (this.activeStatus !== '') {
|
||||
this.appliedFilter.push({
|
||||
attribute_key: 'status',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{
|
||||
id: this.activeStatus,
|
||||
name: this.$t(
|
||||
`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${this.activeStatus}.TEXT`
|
||||
),
|
||||
},
|
||||
],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
if (this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ME) {
|
||||
this.appliedFilter.push({
|
||||
attribute_key: 'assignee_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: this.currentUserDetails,
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
initializeInboxTeamAndLabelFilterToModal() {
|
||||
if (this.conversationInbox) {
|
||||
this.appliedFilter.push({
|
||||
attribute_key: 'inbox_id',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{
|
||||
id: this.conversationInbox,
|
||||
name: this.inbox.name,
|
||||
},
|
||||
],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
if (this.teamId) {
|
||||
this.appliedFilter.push({
|
||||
attribute_key: 'team_id',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: this.activeTeam,
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
if (this.label) {
|
||||
this.appliedFilter.push({
|
||||
attribute_key: 'labels',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: [
|
||||
{
|
||||
id: this.label,
|
||||
name: this.label,
|
||||
},
|
||||
],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import MessageFormatter from '../helpers/MessageFormatter';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
formatMessage(message, isATweet, isAPrivateNote) {
|
||||
const messageFormatter = new MessageFormatter(
|
||||
message,
|
||||
isATweet,
|
||||
isAPrivateNote
|
||||
);
|
||||
return messageFormatter.formattedMessage;
|
||||
},
|
||||
getPlainText(message, isATweet) {
|
||||
const messageFormatter = new MessageFormatter(message, isATweet);
|
||||
return messageFormatter.plainText;
|
||||
},
|
||||
truncateMessage(description = '') {
|
||||
if (description.length < 100) {
|
||||
return description;
|
||||
}
|
||||
|
||||
return `${description.slice(0, 97)}...`;
|
||||
},
|
||||
|
||||
highlightContent(content = '', searchTerm = '', highlightClass = '') {
|
||||
const plainTextContent = this.getPlainText(content);
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
const escapedSearchTerm = searchTerm.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
'\\$&'
|
||||
);
|
||||
|
||||
return plainTextContent.replace(
|
||||
new RegExp(`(${escapedSearchTerm})`, 'ig'),
|
||||
`<span class="${highlightClass}">$1</span>`
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,450 +0,0 @@
|
||||
import methodsMixin from '../../../dashboard/mixins/automations/methodsMixin';
|
||||
import {
|
||||
automation,
|
||||
customAttributes,
|
||||
agents,
|
||||
booleanFilterOptions,
|
||||
teams,
|
||||
labels,
|
||||
statusFilterOptions,
|
||||
campaigns,
|
||||
contacts,
|
||||
inboxes,
|
||||
languages,
|
||||
countries,
|
||||
slaPolicies,
|
||||
MESSAGE_CONDITION_VALUES,
|
||||
automationToSubmit,
|
||||
savedAutomation,
|
||||
} from './automationFixtures';
|
||||
import {
|
||||
AUTOMATIONS,
|
||||
AUTOMATION_ACTION_TYPES,
|
||||
} from '../../../dashboard/routes/dashboard/settings/automation/constants.js';
|
||||
|
||||
import { createWrapper, createLocalVue } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
// Vuelidate required to test submit method
|
||||
|
||||
const createComponent = (
|
||||
mixins,
|
||||
data,
|
||||
// eslint-disable-next-line default-param-last
|
||||
computed = {},
|
||||
// eslint-disable-next-line default-param-last
|
||||
methods = {},
|
||||
validations
|
||||
) => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins,
|
||||
data,
|
||||
computed,
|
||||
methods,
|
||||
validations,
|
||||
};
|
||||
const Constructor = Vue.extend(Component);
|
||||
const vm = new Constructor().$mount();
|
||||
return createWrapper(vm);
|
||||
};
|
||||
|
||||
const generateComputedProperties = () => {
|
||||
return {
|
||||
statusFilterOptions() {
|
||||
return statusFilterOptions;
|
||||
},
|
||||
agents() {
|
||||
return agents;
|
||||
},
|
||||
customAttributes() {
|
||||
return customAttributes;
|
||||
},
|
||||
labels() {
|
||||
return labels;
|
||||
},
|
||||
teams() {
|
||||
return teams;
|
||||
},
|
||||
booleanFilterOptions() {
|
||||
return booleanFilterOptions;
|
||||
},
|
||||
campaigns() {
|
||||
return campaigns;
|
||||
},
|
||||
contacts() {
|
||||
return contacts;
|
||||
},
|
||||
inboxes() {
|
||||
return inboxes;
|
||||
},
|
||||
languages() {
|
||||
return languages;
|
||||
},
|
||||
countries() {
|
||||
return countries;
|
||||
},
|
||||
slaPolicies() {
|
||||
return slaPolicies;
|
||||
},
|
||||
MESSAGE_CONDITION_VALUES() {
|
||||
return MESSAGE_CONDITION_VALUES;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('automationMethodsMixin', () => {
|
||||
it('getFileName returns the correct file name', () => {
|
||||
const data = () => {
|
||||
return {};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
expect(
|
||||
wrapper.vm.getFileName(automation.actions[0], automation.files)
|
||||
).toEqual(automation.files[0].filename);
|
||||
});
|
||||
|
||||
it('getAttributes returns all attributes', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automationTypes: AUTOMATIONS,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
expect(wrapper.vm.getAttributes('conversation_created')).toEqual(
|
||||
AUTOMATIONS.conversation_created.conditions
|
||||
);
|
||||
});
|
||||
|
||||
it('getAttributes returns all respective attributes', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
allCustomAttributes: customAttributes,
|
||||
automationTypes: AUTOMATIONS,
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
expect(wrapper.vm.getInputType('status')).toEqual('multi_select');
|
||||
expect(wrapper.vm.getInputType('my_list')).toEqual('search_select');
|
||||
});
|
||||
|
||||
it('getOperators returns all respective operators', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
allCustomAttributes: customAttributes,
|
||||
automationTypes: AUTOMATIONS,
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
expect(wrapper.vm.getOperators('status')).toEqual(
|
||||
AUTOMATIONS.conversation_created.conditions[0].filterOperators
|
||||
);
|
||||
});
|
||||
|
||||
it('getAutomationType returns the correct automationType', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automationTypes: AUTOMATIONS,
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
expect(wrapper.vm.getAutomationType('status')).toEqual(
|
||||
AUTOMATIONS[automation.event_name].conditions[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('getConditionDropdownValues returns respective condition dropdown values', () => {
|
||||
const computed = generateComputedProperties();
|
||||
const data = () => {
|
||||
return {
|
||||
allCustomAttributes: customAttributes,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data, computed);
|
||||
expect(wrapper.vm.getConditionDropdownValues('status')).toEqual(
|
||||
statusFilterOptions
|
||||
);
|
||||
expect(wrapper.vm.getConditionDropdownValues('team_id')).toEqual(teams);
|
||||
expect(wrapper.vm.getConditionDropdownValues('assignee_id')).toEqual(
|
||||
agents
|
||||
);
|
||||
expect(wrapper.vm.getConditionDropdownValues('contact')).toEqual(contacts);
|
||||
expect(wrapper.vm.getConditionDropdownValues('inbox_id')).toEqual(inboxes);
|
||||
expect(wrapper.vm.getConditionDropdownValues('campaigns')).toEqual(
|
||||
campaigns
|
||||
);
|
||||
expect(wrapper.vm.getConditionDropdownValues('browser_language')).toEqual(
|
||||
languages
|
||||
);
|
||||
expect(wrapper.vm.getConditionDropdownValues('country_code')).toEqual(
|
||||
countries
|
||||
);
|
||||
expect(wrapper.vm.getConditionDropdownValues('message_type')).toEqual(
|
||||
MESSAGE_CONDITION_VALUES
|
||||
);
|
||||
});
|
||||
|
||||
it('appendNewCondition appends a new condition to the automation data property', () => {
|
||||
const condition = {
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
};
|
||||
const data = () => {
|
||||
return {
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
wrapper.vm.appendNewCondition();
|
||||
expect(automation.conditions[automation.conditions.length - 1]).toEqual(
|
||||
condition
|
||||
);
|
||||
});
|
||||
|
||||
it('appendNewAction appends a new condition to the automation data property', () => {
|
||||
const action = {
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
};
|
||||
const data = () => {
|
||||
return {
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
wrapper.vm.appendNewAction();
|
||||
expect(automation.actions[automation.actions.length - 1]).toEqual(action);
|
||||
});
|
||||
|
||||
it('removeFilter removes the given condition in the automation', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
wrapper.vm.removeFilter(0);
|
||||
expect(automation.conditions.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('removeAction removes the given action in the automation', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
wrapper.vm.removeAction(0);
|
||||
expect(automation.actions.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('resetFilter resets the current automation conditions', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automation: automationToSubmit,
|
||||
automationTypes: AUTOMATIONS,
|
||||
};
|
||||
};
|
||||
const conditionAfterReset = {
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
wrapper.vm.resetFilter(0, automationToSubmit.conditions[0]);
|
||||
expect(automation.conditions[0]).toEqual(conditionAfterReset);
|
||||
});
|
||||
|
||||
it('showUserInput returns boolean value based on the operator type', () => {
|
||||
const data = () => {
|
||||
return {};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
expect(wrapper.vm.showUserInput('is_present')).toBeFalsy();
|
||||
expect(wrapper.vm.showUserInput('is_not_present')).toBeFalsy();
|
||||
expect(wrapper.vm.showUserInput('equal_to')).toBeTruthy();
|
||||
expect(wrapper.vm.showUserInput('not_equal_to')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('showActionInput returns boolean value based on the action type', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automationActionTypes: AUTOMATION_ACTION_TYPES,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
expect(wrapper.vm.showActionInput('send_email_to_team')).toBeFalsy();
|
||||
expect(wrapper.vm.showActionInput('send_message')).toBeFalsy();
|
||||
expect(wrapper.vm.showActionInput('send_webhook_event')).toBeTruthy();
|
||||
expect(wrapper.vm.showActionInput('resolve_conversation')).toBeFalsy();
|
||||
expect(wrapper.vm.showActionInput('add_label')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('resetAction resets the action to default state', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automation,
|
||||
};
|
||||
};
|
||||
const wrapper = createComponent([methodsMixin], data);
|
||||
wrapper.vm.resetAction(0);
|
||||
expect(automation.actions[0].action_params).toEqual([]);
|
||||
});
|
||||
|
||||
it('manifestConditions resets the action to default state', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automation: {},
|
||||
allCustomAttributes: customAttributes,
|
||||
automationTypes: AUTOMATIONS,
|
||||
};
|
||||
};
|
||||
const methods = {
|
||||
getConditionDropdownValues() {
|
||||
return statusFilterOptions;
|
||||
},
|
||||
};
|
||||
|
||||
const manifestedConditions = [
|
||||
{
|
||||
values: [
|
||||
{
|
||||
id: 'open',
|
||||
name: 'Open',
|
||||
},
|
||||
],
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
const wrapper = createComponent([methodsMixin], data, {}, methods);
|
||||
expect(wrapper.vm.manifestConditions(savedAutomation)).toEqual(
|
||||
manifestedConditions
|
||||
);
|
||||
});
|
||||
|
||||
it('generateActionsArray return the manifested actions array', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automationActionTypes: AUTOMATION_ACTION_TYPES,
|
||||
};
|
||||
};
|
||||
const computed = {
|
||||
agents() {
|
||||
return agents;
|
||||
},
|
||||
labels() {
|
||||
return labels;
|
||||
},
|
||||
teams() {
|
||||
return teams;
|
||||
},
|
||||
};
|
||||
|
||||
const methods = {
|
||||
getActionDropdownValues() {
|
||||
return [
|
||||
{
|
||||
id: 2,
|
||||
name: 'testlabel',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'snoozes',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
const testAction = {
|
||||
action_name: 'add_label',
|
||||
action_params: [2],
|
||||
};
|
||||
|
||||
const expectedActionArray = [
|
||||
{
|
||||
id: 2,
|
||||
name: 'testlabel',
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = createComponent([methodsMixin], data, computed, methods);
|
||||
expect(wrapper.vm.generateActionsArray(testAction)).toEqual(
|
||||
expectedActionArray
|
||||
);
|
||||
});
|
||||
|
||||
it('manifestActions manifest the received action and generate the correct array', () => {
|
||||
const data = () => {
|
||||
return {
|
||||
automation: {},
|
||||
allCustomAttributes: customAttributes,
|
||||
automationTypes: AUTOMATIONS,
|
||||
};
|
||||
};
|
||||
const methods = {
|
||||
generateActionsArray() {
|
||||
return [
|
||||
{
|
||||
id: 2,
|
||||
name: 'testlabel',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
const expectedActions = [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'testlabel',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const wrapper = createComponent([methodsMixin], data, {}, methods);
|
||||
expect(wrapper.vm.manifestActions(savedAutomation)).toEqual(
|
||||
expectedActions
|
||||
);
|
||||
});
|
||||
|
||||
it('getActionDropdownValues returns Action dropdown Values', () => {
|
||||
const data = () => {
|
||||
return {};
|
||||
};
|
||||
const computed = {
|
||||
agents() {
|
||||
return agents;
|
||||
},
|
||||
labels() {
|
||||
return labels;
|
||||
},
|
||||
teams() {
|
||||
return teams;
|
||||
},
|
||||
slaPolicies() {
|
||||
return slaPolicies;
|
||||
},
|
||||
};
|
||||
const expectedActionDropdownValues = [
|
||||
{ id: 'testlabel', name: 'testlabel' },
|
||||
{ id: 'snoozes', name: 'snoozes' },
|
||||
];
|
||||
const wrapper = createComponent([methodsMixin], data, computed);
|
||||
expect(wrapper.vm.getActionDropdownValues('add_label')).toEqual(
|
||||
expectedActionDropdownValues
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import campaignMixin from '../campaignMixin';
|
||||
|
||||
const buildComponent = routeName => ({
|
||||
template: '<div></div>',
|
||||
computed: {
|
||||
$route() {
|
||||
return { name: routeName };
|
||||
},
|
||||
},
|
||||
mixins: [campaignMixin],
|
||||
});
|
||||
|
||||
describe('campaignMixin', () => {
|
||||
beforeEach(() => {
|
||||
global.window = Object.create(window);
|
||||
});
|
||||
it('returns the correct campaign type', () => {
|
||||
const Component = buildComponent('one_off');
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.campaignType).toBe('one_off');
|
||||
});
|
||||
it('isOneOffType returns true if path is one_off', () => {
|
||||
const Component = buildComponent('one_off');
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isOneOffType).toBe(true);
|
||||
});
|
||||
|
||||
it('isOngoingType returns true if path is ongoing_campaigns', () => {
|
||||
const Component = buildComponent('ongoing_campaigns');
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isOngoingType).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import filterMixin from '../filterMixin';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MockComponent from './MockComponent.vue';
|
||||
|
||||
describe('Test mixin function', () => {
|
||||
const wrapper = shallowMount(MockComponent, {
|
||||
mixins: [filterMixin],
|
||||
});
|
||||
|
||||
it('should return proper value from bool', () => {
|
||||
expect(wrapper.vm.setFilterAttributes).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return proper value from bool', () => {
|
||||
expect(wrapper.vm.initializeExistingFilterToModal).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import messageFormatterMixin from '../messageFormatterMixin';
|
||||
|
||||
describe('messageFormatterMixin', () => {
|
||||
it('returns correct plain text', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [messageFormatterMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
const message =
|
||||
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>';
|
||||
expect(wrapper.vm.getPlainText(message)).toMatch(
|
||||
'Chatwoot is an opensource tool. https://www.chatwoot.com'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import PlaygroundHeader from '../../components/playground/Header.vue';
|
||||
import UserMessage from '../../components/playground/UserMessage.vue';
|
||||
import BotMessage from '../../components/playground/BotMessage.vue';
|
||||
@@ -12,13 +12,18 @@ export default {
|
||||
BotMessage,
|
||||
TypingIndicator,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
componentData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { messages: [], messageContent: '', isWaiting: false };
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import ChatCard from 'shared/components/ChatCard.vue';
|
||||
import ChatForm from 'shared/components/ChatForm.vue';
|
||||
import ChatOptions from 'shared/components/ChatOptions.vue';
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
CustomerSatisfaction,
|
||||
IntegrationCard,
|
||||
},
|
||||
mixins: [messageFormatterMixin, darkModeMixin],
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
message: { type: String, default: null },
|
||||
contentType: { type: String, default: null },
|
||||
@@ -31,6 +31,16 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
|
||||
useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
getPlainText,
|
||||
truncateMessage,
|
||||
highlightContent,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isTemplate() {
|
||||
return this.messageType === 3;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
@@ -11,7 +11,7 @@ import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
export default {
|
||||
name: 'UnreadMessage',
|
||||
components: { Thumbnail },
|
||||
mixins: [messageFormatterMixin, configMixin, darkModeMixin],
|
||||
mixins: [configMixin, darkModeMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
@@ -30,6 +30,12 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
companyName() {
|
||||
return `${this.$t('UNREAD_VIEW.COMPANY_FROM')} ${
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
name: 'UserMessageBubble',
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
@@ -15,6 +14,12 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
return {
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
|
||||
@@ -7,13 +7,19 @@ export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [messageFormatterMixin, darkModeMixin],
|
||||
mixins: [darkModeMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { truncateMessage } = useMessageFormatter();
|
||||
return {
|
||||
truncateMessage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user