Compare commits

..
Author SHA1 Message Date
Fayaz AhmedandGitHub 084a470f3b Merge branch 'develop' into chore-repalce-configMixin-with-useConfig-composable 2024-08-28 08:26:01 +05:30
3b5f5b41ad chore: Replace campaign mixin with composable [CW-3463] (#9987)
# Pull Request Template

## Description

Repalces campaignMixin and its usage with the new useCampaign mixin

Fixes
https://linear.app/chatwoot/issue/CW-3463/rewrite-campaignmixin-mixin-to-a-composable

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-08-28 00:53:18 +05:30
fe5670832a chore: Replace filtersMixin with useFilter composable [CW-3466] (#10036)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-08-27 13:50:25 +05:30
bc6420019f feat: Rewrite automations/methodsMixin to a composable (#9956)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-08-27 12:30:08 +05:30
Fayaz Ahmed f9b330fa3f refactor: introduced a feature check method 2024-08-27 08:36:12 +05:30
Fayaz AhmedandGitHub f82ec3b885 chore: Repalce message formatter mixin with useMessageFormatter [CW-3470] (#9986)
# Pull Request Template

## Description

Replaced the old messageFormatterMixin with a useMessageFormatter
composable
2024-08-27 08:06:51 +05:30
Sivin VargheseandGitHub 32c25047c4 feat: Rewrite reportMixin to a composable (#10029)
# Pull Request Template

## Description

The PR will replace the usage of `reportMixin` with the help of
`useReportMetrics()` composable.

Fixes
https://linear.app/chatwoot/issue/CW-3450/rewrite-reportmixin-mixin-to-a-composable

**Files updated**
1. dashboard/routes/dashboard/settings/reports/Index.vue
2. dashboard/routes/dashboard/settings/reports/BotReports.vue
3. dashboard/routes/dashboard/settings/reports/ReportContainer.vue
4.
dashboard/routes/dashboard/settings/reports/components/WootReports.vue
5.
dashboard/routes/dashboard/settings/reports/components/ChartElements/ChartStats.vue

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Test the all the reports view.


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-08-27 08:00:05 +05:30
7f8d718da3 feat: Rewrite command bar mixin to a composable (#10015)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2024-08-26 15:55:59 +05:30
3489783cb8 feat: add domain blocklist feature (#10016)
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-08-26 13:05:36 +05:30
Muhsin KelothandGitHub 53d68868c6 chore: Hide linear linked issues error toast messages (#10020)
We are fetching linked Linear issues when opening a conversation if Linear integration is enabled. There may be some cases where the API call fails. We don't need to show an error message every time a user opens the conversation, as it's not critical. However, when someone clicks on the Linear icon, we can inform them that the integration is disabled. This PR will fix the issue.
2024-08-23 17:19:06 +05:30
b61ad6e41a feat: Add APIs to manage custom roles in Chatwoot (#9995)
Co-authored-by: Pranav <pranavrajs@gmail.com>
2024-08-23 17:18:28 +05:30
Fayaz AhmedandGitHub 462064bfc0 Merge branch 'develop' into chore-repalce-configMixin-with-useConfig-composable 2024-08-21 08:22:44 +05:30
Fayaz Ahmed 7db7ae0ed8 chore: Replace widget config mixin with useConfig composable 2024-08-13 19:04:55 +05:30
128 changed files with 3214 additions and 1806 deletions
+19
View File
@@ -32,6 +32,8 @@ class AccountBuilder
end
def validate_email
raise InvalidEmail.new({ domain_blocked: domain_blocked }) if domain_blocked?
address = ValidEmail2::Address.new(@email)
if address.valid? && !address.disposable?
true
@@ -79,4 +81,21 @@ class AccountBuilder
@user.confirm if @confirmed
@user.save!
end
def domain_blocked?
domain = @email.split('@').last
blocked_domains.each do |blocked_domain|
return true if domain.match?(blocked_domain)
end
false
end
def blocked_domains
domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '')
domains.split("\n").map(&:strip) if domains.present?
[]
end
end
@@ -24,7 +24,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
def update
@agent.update!(agent_params.slice(:name).compact)
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact)
@agent.current_account_user.update!(agent_params.slice(*account_user_attributes).compact)
end
def destroy
@@ -67,8 +67,16 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
@agent = agents.find(params[:id])
end
def account_user_attributes
[:role, :availability, :auto_offline]
end
def allowed_agent_params
[:name, :email, :name, :role, :availability, :auto_offline]
end
def agent_params
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
params.require(:agent).permit(allowed_agent_params)
end
def new_agent_params
@@ -101,3 +109,5 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
DeleteObjectJob.perform_later(agent) if agent.reload.account_users.blank?
end
end
Api::V1::Accounts::AgentsController.prepend_mod_with('Api::V1::Accounts::AgentsController')
@@ -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>
@@ -13,7 +13,7 @@ import wootConstants from 'dashboard/constants/globals';
import {
CMD_REOPEN_CONVERSATION,
CMD_RESOLVE_CONVERSATION,
} from 'dashboard/helper/commandBar/events';
} from 'dashboard/helper/commandbar/events';
const store = useStore();
const getters = useStoreGetters();
@@ -7,7 +7,7 @@ import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useAI } from 'dashboard/composables/useAI';
import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue';
import { CMD_AI_ASSIST } from 'dashboard/helper/commandBar/events';
import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
export default {
@@ -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 {
@@ -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;
@@ -7,7 +7,7 @@ import {
CMD_MUTE_CONVERSATION,
CMD_SEND_TRANSCRIPT,
CMD_UNMUTE_CONVERSATION,
} from 'dashboard/helper/commandBar/events';
} from 'dashboard/helper/commandbar/events';
export default {
components: {
@@ -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,
@@ -6,7 +6,7 @@ import {
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
CMD_BULK_ACTION_REOPEN_CONVERSATION,
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
} from 'dashboard/helper/commandBar/events';
} from 'dashboard/helper/commandbar/events';
import AgentSelector from './AgentSelector.vue';
import UpdateActions from './UpdateActions.vue';
@@ -48,11 +48,7 @@ const loadLinkedIssue = async () => {
const issues = response.data;
linkedIssue.value = issues && issues.length ? issues[0] : null;
} catch (error) {
const errorMessage = parseLinearAPIErrorResponse(
error,
t('INTEGRATION_SETTINGS.LINEAR.LOADING_ERROR')
);
useAlert(errorMessage);
// We don't want to show an error message here, as it's not critical. When someone clicks on the Linear icon, we can inform them that the integration is disabled.
}
};
@@ -184,3 +184,14 @@ export const mockInactiveLabels = [
show_on_sidebar: true,
},
];
export const MOCK_FEATURE_FLAGS = {
CRM: 'crm',
AGENT_MANAGEMENT: 'agent_management',
TEAM_MANAGEMENT: 'team_management',
INBOX_MANAGEMENT: 'inbox_management',
REPORTS: 'reports',
LABELS: 'labels',
CANNED_RESPONSES: 'canned_responses',
INTEGRATIONS: 'integrations',
};
@@ -27,14 +27,6 @@ describe('useBulkActionsHotKeys', () => {
emitter.emit = vi.fn();
});
it('should return the correct computed properties', () => {
const { selectedConversations, bulkActionsHotKeys } =
useBulkActionsHotKeys();
expect(selectedConversations.value).toEqual([]);
expect(bulkActionsHotKeys.value).toEqual([]);
});
it('should return bulk actions when conversations are selected', () => {
store.getters['bulkActions/getSelectedConversationIds'] = [1, 2, 3];
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
@@ -57,24 +57,8 @@ describe('useConversationHotKeys', () => {
});
it('should return the correct computed properties', () => {
const {
assignableAgents,
currentChat,
teamsList,
activeLabels,
draftMessage,
replyMode,
contextMenuChatId,
conversationHotKeys,
} = useConversationHotKeys();
const { conversationHotKeys } = useConversationHotKeys();
expect(assignableAgents.value).toEqual(mockAssignableAgents);
expect(currentChat.value).toEqual(mockCurrentChat);
expect(teamsList.value).toEqual(mockTeamsList);
expect(activeLabels.value).toEqual(mockActiveLabels);
expect(draftMessage.value).toBeUndefined();
expect(replyMode.value).toBe(REPLY_EDITOR_MODES.REPLY);
expect(contextMenuChatId.value).toBeNull();
expect(conversationHotKeys.value).toBeDefined();
});
@@ -0,0 +1,188 @@
import { useGoToCommandHotKeys } from '../useGoToCommandHotKeys';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import { useRouter } from 'dashboard/composables/route';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { MOCK_FEATURE_FLAGS } from './fixtures';
vi.mock('dashboard/composables/store');
vi.mock('dashboard/composables/useI18n');
vi.mock('dashboard/composables/route');
vi.mock('dashboard/composables/useAdmin');
vi.mock('dashboard/helper/URLHelper');
const mockRoutes = [
{ path: 'accounts/:accountId/dashboard', name: 'dashboard' },
{
path: 'accounts/:accountId/contacts',
name: 'contacts',
featureFlag: MOCK_FEATURE_FLAGS.CRM,
},
{
path: 'accounts/:accountId/settings/agents/list',
name: 'agent_settings',
featureFlag: MOCK_FEATURE_FLAGS.AGENT_MANAGEMENT,
},
{
path: 'accounts/:accountId/settings/teams/list',
name: 'team_settings',
featureFlag: MOCK_FEATURE_FLAGS.TEAM_MANAGEMENT,
},
{
path: 'accounts/:accountId/settings/inboxes/list',
name: 'inbox_settings',
featureFlag: MOCK_FEATURE_FLAGS.INBOX_MANAGEMENT,
},
{ path: 'accounts/:accountId/profile/settings', name: 'profile_settings' },
{ path: 'accounts/:accountId/notifications', name: 'notifications' },
{
path: 'accounts/:accountId/reports/overview',
name: 'reports_overview',
featureFlag: MOCK_FEATURE_FLAGS.REPORTS,
},
{
path: 'accounts/:accountId/settings/labels/list',
name: 'label_settings',
featureFlag: MOCK_FEATURE_FLAGS.LABELS,
},
{
path: 'accounts/:accountId/settings/canned-response/list',
name: 'canned_responses',
featureFlag: MOCK_FEATURE_FLAGS.CANNED_RESPONSES,
},
{
path: 'accounts/:accountId/settings/applications',
name: 'applications',
featureFlag: MOCK_FEATURE_FLAGS.INTEGRATIONS,
},
];
describe('useGoToCommandHotKeys', () => {
let store;
beforeEach(() => {
store = {
getters: {
getCurrentAccountId: 1,
'accounts/isFeatureEnabledonAccount': vi.fn().mockReturnValue(true),
},
};
useStore.mockReturnValue(store);
useMapGetter.mockImplementation(key => ({
value: store.getters[key],
}));
useI18n.mockReturnValue({ t: vi.fn(key => key) });
useRouter.mockReturnValue({ push: vi.fn() });
useAdmin.mockReturnValue({ isAdmin: { value: true } });
frontendURL.mockImplementation(url => url);
});
it('should return goToCommandHotKeys computed property', () => {
const { goToCommandHotKeys } = useGoToCommandHotKeys();
expect(goToCommandHotKeys.value).toBeDefined();
expect(goToCommandHotKeys.value.length).toBeGreaterThan(0);
});
it('should filter commands based on feature flags', () => {
store.getters['accounts/isFeatureEnabledonAccount'] = vi.fn(
(accountId, flag) => flag !== MOCK_FEATURE_FLAGS.CRM
);
const { goToCommandHotKeys } = useGoToCommandHotKeys();
mockRoutes.forEach(route => {
const command = goToCommandHotKeys.value.find(cmd =>
cmd.id.includes(route.name)
);
if (route.featureFlag === MOCK_FEATURE_FLAGS.CRM) {
expect(command).toBeUndefined();
} else if (!route.featureFlag) {
expect(command).toBeDefined();
}
});
});
it('should filter commands for non-admin users', () => {
useAdmin.mockReturnValue({ isAdmin: { value: false } });
const { goToCommandHotKeys } = useGoToCommandHotKeys();
const adminOnlyCommands = goToCommandHotKeys.value.filter(
cmd =>
cmd.id.includes('agent_settings') ||
cmd.id.includes('team_settings') ||
cmd.id.includes('inbox_settings')
);
expect(adminOnlyCommands.length).toBe(0);
});
it('should include commands for both admin and agent roles when user is admin', () => {
const { goToCommandHotKeys } = useGoToCommandHotKeys();
const adminCommand = goToCommandHotKeys.value.find(cmd =>
cmd.id.includes('agent_settings')
);
const agentCommand = goToCommandHotKeys.value.find(cmd =>
cmd.id.includes('profile_settings')
);
expect(adminCommand).toBeDefined();
expect(agentCommand).toBeDefined();
});
it('should translate section and title for each command', () => {
const { goToCommandHotKeys } = useGoToCommandHotKeys();
goToCommandHotKeys.value.forEach(command => {
expect(useI18n().t).toHaveBeenCalledWith(
expect.stringContaining('COMMAND_BAR.SECTIONS.')
);
expect(useI18n().t).toHaveBeenCalledWith(
expect.stringContaining('COMMAND_BAR.COMMANDS.')
);
expect(command.section).toBeDefined();
expect(command.title).toBeDefined();
});
});
it('should call router.push with correct URL when handler is called', () => {
const { goToCommandHotKeys } = useGoToCommandHotKeys();
goToCommandHotKeys.value.forEach(command => {
command.handler();
expect(useRouter().push).toHaveBeenCalledWith(expect.any(String));
});
});
it('should use current account ID in the path', () => {
store.getters.getCurrentAccountId = 42;
const { goToCommandHotKeys } = useGoToCommandHotKeys();
goToCommandHotKeys.value.forEach(command => {
command.handler();
expect(useRouter().push).toHaveBeenCalledWith(
expect.stringContaining('42')
);
});
});
it('should include icon for each command', () => {
const { goToCommandHotKeys } = useGoToCommandHotKeys();
goToCommandHotKeys.value.forEach(command => {
expect(command.icon).toBeDefined();
});
});
it('should return commands for all enabled features', () => {
const { goToCommandHotKeys } = useGoToCommandHotKeys();
const enabledFeatureCommands = goToCommandHotKeys.value.filter(cmd =>
mockRoutes.some(route => route.featureFlag && cmd.id.includes(route.name))
);
expect(enabledFeatureCommands.length).toBeGreaterThan(0);
});
it('should not return commands for disabled features', () => {
store.getters['accounts/isFeatureEnabledonAccount'] = vi.fn(() => false);
const { goToCommandHotKeys } = useGoToCommandHotKeys();
const disabledFeatureCommands = goToCommandHotKeys.value.filter(cmd =>
mockRoutes.some(route => route.featureFlag && cmd.id.includes(route.name))
);
expect(disabledFeatureCommands.length).toBe(0);
});
});
@@ -0,0 +1,37 @@
import { useInboxHotKeys } from '../useInboxHotKeys';
import { useI18n } from 'dashboard/composables/useI18n';
import { useRoute } from 'dashboard/composables/route';
import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers';
vi.mock('dashboard/composables/useI18n');
vi.mock('dashboard/composables/route');
vi.mock('dashboard/helper/routeHelpers');
vi.mock('shared/helpers/mitt');
describe('useInboxHotKeys', () => {
beforeEach(() => {
useI18n.mockReturnValue({ t: vi.fn(key => key) });
useRoute.mockReturnValue({ name: 'inbox_dashboard' });
isAInboxViewRoute.mockReturnValue(true);
});
it('should return inbox hot keys when on an inbox view route', () => {
const { inboxHotKeys } = useInboxHotKeys();
expect(inboxHotKeys.value.length).toBeGreaterThan(0);
expect(inboxHotKeys.value[0].id).toBe('snooze_notification');
});
it('should return an empty array when not on an inbox view route', () => {
isAInboxViewRoute.mockReturnValue(false);
const { inboxHotKeys } = useInboxHotKeys();
expect(inboxHotKeys.value).toEqual([]);
});
it('should have the correct structure for snooze actions', () => {
const { inboxHotKeys } = useInboxHotKeys();
const snoozeNotificationAction = inboxHotKeys.value.find(
action => action.id === 'snooze_notification'
);
expect(snoozeNotificationAction).toBeDefined();
});
});
@@ -5,7 +5,7 @@ import {
ICON_LIGHT_MODE,
ICON_DARK_MODE,
ICON_SYSTEM_MODE,
} from 'dashboard/helper/commandBar/icons';
} from 'dashboard/helper/commandbar/icons';
import { LocalStorage } from 'shared/helpers/localStorage';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
@@ -7,15 +7,15 @@ import {
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
CMD_BULK_ACTION_REOPEN_CONVERSATION,
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
} from 'dashboard/helper/commandBar/events';
} from 'dashboard/helper/commandbar/events';
import {
ICON_SNOOZE_CONVERSATION,
ICON_REOPEN_CONVERSATION,
ICON_RESOLVE_CONVERSATION,
} from 'dashboard/helper/commandBar/icons';
} from 'dashboard/helper/commandbar/icons';
import { emitter } from 'shared/helpers/mitt';
import { createSnoozeHandlers } from 'dashboard/helper/commandBar/actions';
import { createSnoozeHandlers } from 'dashboard/helper/commandbar/actions';
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
@@ -84,7 +84,6 @@ export function useBulkActionsHotKeys() {
});
return {
selectedConversations,
bulkActionsHotKeys,
};
}
@@ -6,7 +6,7 @@ import { emitter } from 'shared/helpers/mitt';
import { useConversationLabels } from 'dashboard/composables/useConversationLabels';
import { useAI } from 'dashboard/composables/useAI';
import { useAgentsList } from 'dashboard/composables/useAgentsList';
import { CMD_AI_ASSIST } from 'dashboard/helper/commandBar/events';
import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import wootConstants from 'dashboard/constants/globals';
@@ -27,7 +27,7 @@ import {
ICON_AI_SHORTEN,
ICON_AI_EXPAND,
ICON_AI_GRAMMAR,
} from 'dashboard/helper/commandBar/icons';
} from 'dashboard/helper/commandbar/icons';
import {
OPEN_CONVERSATION_ACTIONS,
@@ -36,7 +36,7 @@ import {
SEND_TRANSCRIPT_ACTION,
UNMUTE_ACTION,
MUTE_ACTION,
} from 'dashboard/helper/commandBar/actions';
} from 'dashboard/helper/commandbar/actions';
import {
isAConversationRoute,
isAInboxViewRoute,
@@ -152,7 +152,7 @@ export function useConversationHotKeys() {
} = useConversationLabels();
const { isAIIntegrationEnabled } = useAI();
const { agentsList, assignableAgents } = useAgentsList();
const { agentsList } = useAgentsList();
const currentChat = useMapGetter('getSelectedChat');
const replyMode = useMapGetter('draftMessages/getReplyEditorMode');
@@ -403,13 +403,6 @@ export function useConversationHotKeys() {
});
return {
assignableAgents,
currentChat,
teamsList,
activeLabels,
draftMessage,
replyMode,
contextMenuChatId,
conversationHotKeys,
};
}
@@ -19,7 +19,7 @@ import {
ICON_TEAM_REPORTS,
ICON_USER_PROFILE,
ICON_CONVERSATION_REPORTS,
} from 'dashboard/helper/commandBar/icons';
} from 'dashboard/helper/commandbar/icons';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -3,8 +3,8 @@ import { useI18n } from 'dashboard/composables/useI18n';
import { useRoute } from 'dashboard/composables/route';
import wootConstants from 'dashboard/constants/globals';
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandBar/events';
import { ICON_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandBar/icons';
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/events';
import { ICON_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/icons';
import { emitter } from 'shared/helpers/mitt';
import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers';
@@ -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;
};
@@ -8,7 +8,7 @@ import {
CMD_SEND_TRANSCRIPT,
CMD_SNOOZE_CONVERSATION,
CMD_UNMUTE_CONVERSATION,
} from './events';
} from 'dashboard/helper/commandbar/events';
import {
ICON_MUTE_CONVERSATION,
@@ -17,7 +17,7 @@ import {
ICON_SEND_TRANSCRIPT,
ICON_SNOOZE_CONVERSATION,
ICON_UNMUTE_CONVERSATION,
} from './icons';
} from 'dashboard/helper/commandbar/icons';
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
@@ -46,6 +46,7 @@ export const SNOOZE_CONVERSATION_ACTIONS = [
{
id: 'snooze_conversation',
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
icon: ICON_SNOOZE_CONVERSATION,
children: Object.values(SNOOZE_OPTIONS),
},
@@ -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);
});
});
@@ -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,
@@ -6,7 +6,7 @@ import { useI18n } from 'dashboard/composables/useI18n';
import { useEmitter } from 'dashboard/composables/emitter';
import { getUnixTime } from 'date-fns';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/helper/commandBar/events';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/helper/commandbar/events';
import wootConstants from 'dashboard/constants/globals';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
@@ -1,9 +1,8 @@
<script setup>
import '@chatwoot/ninja-keys';
import { ref, computed, watch, onMounted } from 'vue';
import { ref, computed, watchEffect, onMounted } from 'vue';
import { useStore } from 'dashboard/composables/store';
import { useTrack } from 'dashboard/composables';
import { useRoute } from 'dashboard/composables/route';
import { useI18n } from 'dashboard/composables/useI18n';
import { useAppearanceHotKeys } from 'dashboard/composables/commands/useAppearanceHotKeys';
import { useInboxHotKeys } from 'dashboard/composables/commands/useInboxHotKeys';
@@ -14,7 +13,6 @@ import wootConstants from 'dashboard/constants/globals';
import { GENERAL_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
const store = useStore();
const route = useRoute();
const track = useTrack();
const { t } = useI18n();
@@ -28,20 +26,10 @@ const selectedSnoozeType = ref(null);
const { goToAppearanceHotKeys } = useAppearanceHotKeys();
const { inboxHotKeys } = useInboxHotKeys();
const { goToCommandHotKeys } = useGoToCommandHotKeys();
const { bulkActionsHotKeys, selectedConversations } = useBulkActionsHotKeys();
const {
conversationHotKeys,
assignableAgents,
currentChat,
teamsList,
activeLabels,
draftMessage,
replyMode,
contextMenuChatId,
} = useConversationHotKeys();
const { bulkActionsHotKeys } = useBulkActionsHotKeys();
const { conversationHotKeys } = useConversationHotKeys();
const placeholder = computed(() => t('COMMAND_BAR.SEARCH_PLACEHOLDER'));
const routeName = computed(() => route.name);
const hotKeys = computed(() => [
...inboxHotKeys.value,
@@ -85,22 +73,11 @@ const onClosed = () => {
}
};
watch(
[
routeName,
selectedConversations,
assignableAgents,
currentChat,
teamsList,
activeLabels,
draftMessage,
replyMode,
contextMenuChatId,
],
() => {
setCommandBarData();
watchEffect(() => {
if (ninjakeys.value) {
ninjakeys.value.data = hotKeys.value;
}
);
});
onMounted(setCommandBarData);
</script>
@@ -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,
@@ -2,7 +2,7 @@
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { getUnixTime } from 'date-fns';
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandBar/events';
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/helper/commandbar/events';
import wootConstants from 'dashboard/constants/globals';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
@@ -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>
@@ -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: '' },
},
@@ -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: [] };
},
@@ -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') }}
@@ -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;
},
},
};
-119
View File
@@ -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 };
},
+8 -2
View File
@@ -3,7 +3,7 @@ import { mapGetters, mapActions } from 'vuex';
import { setHeader } from 'widget/helpers/axios';
import addHours from 'date-fns/addHours';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import configMixin from './mixins/configMixin';
import { useConfig } from 'widget/composables/useConfig';
import availabilityMixin from 'widget/mixins/availability';
import { getLocale } from './helpers/urlParamsHelper';
import { isEmptyObject } from 'widget/helpers/utils';
@@ -27,7 +27,13 @@ export default {
components: {
Spinner,
},
mixins: [availabilityMixin, configMixin, routerMixin, darkModeMixin],
mixins: [availabilityMixin, routerMixin, darkModeMixin],
setup() {
const { shouldShowPreChatForm } = useConfig();
return {
shouldShowPreChatForm,
};
},
data() {
return {
isMobile: false,
@@ -8,8 +8,8 @@ import VideoBubble from 'widget/components/VideoBubble.vue';
import FileBubble from 'widget/components/FileBubble.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import configMixin from '../mixins/configMixin';
import messageMixin from '../mixins/messageMixin';
import { useConfig } from 'widget/composables/useConfig';
import { useMessage } from '../composables/useMessage';
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import ReplyToChip from 'widget/components/ReplyToChip.vue';
@@ -28,7 +28,7 @@ export default {
MessageReplyButton,
ReplyToChip,
},
mixins: [configMixin, messageMixin, darkModeMixin],
mixins: [darkModeMixin],
props: {
message: {
type: Object,
@@ -39,6 +39,19 @@ export default {
default: () => {},
},
},
setup(props) {
const { messageContentAttributes, hasAttachments } = useMessage(
props.message
);
const { useInboxAvatarForBot, inboxAvatarUrl, channelConfig } = useConfig();
return {
messageContentAttributes,
hasAttachments,
useInboxAvatarForBot,
inboxAvatarUrl,
channelConfig,
};
},
data() {
return {
hasImageError: 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;
@@ -3,7 +3,7 @@ import { mapGetters } from 'vuex';
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
import ChatSendButton from 'widget/components/ChatSendButton.vue';
import configMixin from '../mixins/configMixin';
import { useConfig } from 'widget/composables/useConfig';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
@@ -19,7 +19,7 @@ export default {
FluentIcon,
ResizableTextArea,
},
mixins: [configMixin, darkModeMixin],
mixins: [darkModeMixin],
props: {
onSendMessage: {
type: Function,
@@ -30,7 +30,13 @@ export default {
default: () => {},
},
},
setup() {
const { hasEmojiPickerEnabled, hasAttachmentsEnabled } = useConfig();
return {
hasEmojiPickerEnabled,
hasAttachmentsEnabled,
};
},
data() {
return {
userInput: '',
@@ -4,13 +4,13 @@ import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import { popoutChatWindow } from '../helpers/popoutHelper';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import configMixin from 'widget/mixins/configMixin';
import { useConfig } from 'widget/composables/useConfig';
import { CONVERSATION_STATUS } from 'shared/constants/messages';
export default {
name: 'HeaderActions',
components: { FluentIcon },
mixins: [configMixin, darkModeMixin],
mixins: [darkModeMixin],
props: {
showPopoutButton: {
type: Boolean,
@@ -21,6 +21,12 @@ export default {
default: true,
},
},
setup() {
const { hasEndConversationEnabled } = useConfig();
return {
hasEndConversationEnabled,
};
},
computed: {
...mapGetters({
conversationAttributes: 'conversationAttributes/getConversationParams',
@@ -5,23 +5,31 @@ import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import { isEmptyObject } from 'widget/helpers/utils';
import { getRegexp } from 'shared/helpers/Validators';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import routerMixin from 'widget/mixins/routerMixin';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import configMixin from 'widget/mixins/configMixin';
import { useConfig } from 'widget/composables/useConfig';
export default {
components: {
CustomButton,
Spinner,
},
mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin],
mixins: [routerMixin, darkModeMixin, messageFormatterMixin],
props: {
options: {
type: Object,
default: () => {},
},
},
setup() {
const { preChatFormEnabled } = useConfig();
const { formatMessage } = useMessageFormatter();
return {
preChatFormEnabled,
formatMessage,
};
},
data() {
return {
locale: this.$root.$i18n.locale,
@@ -3,7 +3,7 @@ import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
import AvailableAgents from 'widget/components/AvailableAgents.vue';
import configMixin from 'widget/mixins/configMixin';
import { useConfig } from 'widget/composables/useConfig';
import availabilityMixin from 'widget/mixins/availability';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import { IFrameHelper } from 'widget/helpers/utils';
@@ -15,7 +15,7 @@ export default {
AvailableAgents,
FluentIcon,
},
mixins: [configMixin, nextAvailabilityTime, availabilityMixin],
mixins: [nextAvailabilityTime, availabilityMixin],
props: {
availableAgents: {
type: Array,
@@ -26,7 +26,12 @@ export default {
default: false,
},
},
setup() {
const { channelConfig } = useConfig();
return {
channelConfig,
};
},
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
@@ -1,7 +1,7 @@
<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 { useConfig } from 'widget/composables/useConfig';
import { isEmptyObject } from 'widget/helpers/utils';
import {
ON_CAMPAIGN_MESSAGE_CLICK,
@@ -11,7 +11,7 @@ import darkModeMixin from 'widget/mixins/darkModeMixin';
export default {
name: 'UnreadMessage',
components: { Thumbnail },
mixins: [messageFormatterMixin, configMixin, darkModeMixin],
mixins: [messageFormatterMixin, darkModeMixin],
props: {
message: {
type: String,
@@ -30,6 +30,16 @@ export default {
default: null,
},
},
setup() {
const { useInboxAvatarForBot, inboxAvatarUrl, channelConfig } = useConfig();
const { formatMessage } = useMessageFormatter();
return {
useInboxAvatarForBot,
inboxAvatarUrl,
channelConfig,
formatMessage,
};
},
computed: {
companyName() {
return `${this.$t('UNREAD_VIEW.COMPANY_FROM')} ${
@@ -1,6 +1,5 @@
<script>
import { mapGetters } from 'vuex';
import configMixin from '../mixins/configMixin';
import { ON_UNREAD_MESSAGE_CLICK } from '../constants/widgetBusEvents';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import UnreadMessage from 'widget/components/UnreadMessage.vue';
@@ -12,7 +11,6 @@ export default {
FluentIcon,
UnreadMessage,
},
mixins: [configMixin],
props: {
messages: {
type: Array,
@@ -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);
@@ -3,7 +3,7 @@ import Banner from '../Banner.vue';
import Branding from 'shared/components/Branding.vue';
import ChatHeader from '../ChatHeader.vue';
import ChatHeaderExpanded from '../ChatHeaderExpanded.vue';
import configMixin from '../../mixins/configMixin';
import { useConfig } from 'widget/composables/useConfig';
import { mapGetters } from 'vuex';
import { IFrameHelper } from 'widget/helpers/utils';
@@ -14,7 +14,12 @@ export default {
ChatHeader,
ChatHeaderExpanded,
},
mixins: [configMixin],
setup() {
const { channelConfig } = useConfig();
return {
channelConfig,
};
},
data() {
return {
showPopoutButton: false,
@@ -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>
@@ -0,0 +1,118 @@
import { useConfig } from '../useConfig';
describe('useConfig composable', () => {
beforeEach(() => {
global.window.chatwootWebChannel = {
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: [
'emoji_picker',
'attachments',
'end_conversation',
'use_inbox_avatar_for_bot',
],
preChatFormOptions: {
pre_chat_message: '',
pre_chat_fields: [
{
label: 'Email Id',
name: 'emailAddress',
type: 'email',
field_type: 'standard',
required: false,
enabled: false,
},
{
label: 'Full name',
name: 'fullName',
type: 'text',
field_type: 'standard',
required: true,
enabled: true,
},
],
},
preChatFormEnabled: true,
};
});
it('should return correct values for computed properties', () => {
const {
useInboxAvatarForBot,
hasAConnectedAgentBot,
inboxAvatarUrl,
channelConfig,
hasEmojiPickerEnabled,
hasAttachmentsEnabled,
hasEndConversationEnabled,
preChatFormEnabled,
preChatFormOptions,
shouldShowPreChatForm,
} = useConfig();
expect(useInboxAvatarForBot.value).toBe(true);
expect(hasAConnectedAgentBot.value).toBe(true);
expect(inboxAvatarUrl.value).toBe('https://test.url');
expect(channelConfig.value).toEqual({
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: [
'emoji_picker',
'attachments',
'end_conversation',
'use_inbox_avatar_for_bot',
],
preChatFormOptions: {
pre_chat_message: '',
pre_chat_fields: [
{
label: 'Email Id',
name: 'emailAddress',
type: 'email',
field_type: 'standard',
required: false,
enabled: false,
},
{
label: 'Full name',
name: 'fullName',
type: 'text',
field_type: 'standard',
required: true,
enabled: true,
},
],
},
preChatFormEnabled: true,
});
expect(hasEmojiPickerEnabled.value).toBe(true);
expect(hasAttachmentsEnabled.value).toBe(true);
expect(hasEndConversationEnabled.value).toBe(true);
expect(preChatFormEnabled.value).toBe(true);
expect(preChatFormOptions.value).toEqual({
preChatMessage: '',
preChatFields: [
{
label: 'Email Id',
name: 'emailAddress',
type: 'email',
field_type: 'standard',
required: false,
enabled: false,
},
{
label: 'Full name',
name: 'fullName',
type: 'text',
field_type: 'standard',
required: true,
enabled: true,
},
],
});
expect(shouldShowPreChatForm.value).toBe(true);
});
});
@@ -0,0 +1,81 @@
import { computed } from 'vue';
/**
* Composable for handling config-related computations.
* @returns {Object} An object containing computed properties for various configuration settings.
*/
export function useConfig() {
/**
* Computed property to get the channel config.
* @returns {Object} The channel configuration object.
*/
const channelConfig = computed(() => window.chatwootWebChannel);
const createFeatureChecker = feature =>
computed(() => channelConfig.value.enabledFeatures.includes(feature));
// Use the helper function for feature checks
const useInboxAvatarForBot = createFeatureChecker('use_inbox_avatar_for_bot');
const hasEmojiPickerEnabled = createFeatureChecker('emoji_picker');
const hasAttachmentsEnabled = createFeatureChecker('attachments');
const hasEndConversationEnabled = createFeatureChecker('end_conversation');
/**
* Computed property to check if the agent bot is connected.
* @returns {Boolean} True if the agent bot is connected, false otherwise.
*/
const hasAConnectedAgentBot = computed(
() => !!channelConfig.value.hasAConnectedAgentBot
);
/**
* Computed property to get the inbox avatar URL.
* @returns {String} The inbox avatar URL.
*/
const inboxAvatarUrl = computed(() => channelConfig.value.avatarUrl);
/**
* Computed property to check if the pre-chat form is enabled.
* @returns {Boolean} True if the pre-chat form is enabled, false otherwise.
*/
const preChatFormEnabled = computed(
() => channelConfig.value.preChatFormEnabled
);
/**
* Computed property to get the pre-chat form options.
* @returns {Object} An object containing pre-chat message and fields.
*/
const preChatFormOptions = computed(() => {
const options = channelConfig.value.preChatFormOptions || {};
const preChatMessage = options.pre_chat_message || '';
const preChatFields = options.pre_chat_fields || [];
return {
preChatMessage,
preChatFields,
};
});
/**
* Computed property to determine if the pre-chat form should be shown.
* @returns {Boolean} True if the pre-chat form should be shown, false otherwise.
*/
const shouldShowPreChatForm = computed(() => {
if (!preChatFormEnabled.value) return false;
const { preChatFields } = preChatFormOptions.value;
return preChatFields.some(field => field.enabled);
});
return {
useInboxAvatarForBot,
hasAConnectedAgentBot,
inboxAvatarUrl,
channelConfig,
hasEmojiPickerEnabled,
hasAttachmentsEnabled,
hasEndConversationEnabled,
preChatFormEnabled,
preChatFormOptions,
shouldShowPreChatForm,
};
}
@@ -1,47 +0,0 @@
export default {
computed: {
useInboxAvatarForBot() {
return this.channelConfig.enabledFeatures.includes(
'use_inbox_avatar_for_bot'
);
},
hasAConnectedAgentBot() {
return !!window.chatwootWebChannel.hasAConnectedAgentBot;
},
inboxAvatarUrl() {
return window.chatwootWebChannel.avatarUrl;
},
channelConfig() {
return window.chatwootWebChannel;
},
hasEmojiPickerEnabled() {
return this.channelConfig.enabledFeatures.includes('emoji_picker');
},
hasAttachmentsEnabled() {
return this.channelConfig.enabledFeatures.includes('attachments');
},
hasEndConversationEnabled() {
return this.channelConfig.enabledFeatures.includes('end_conversation');
},
preChatFormEnabled() {
return window.chatwootWebChannel.preChatFormEnabled;
},
preChatFormOptions() {
let preChatMessage = '';
const options = window.chatwootWebChannel.preChatFormOptions || {};
preChatMessage = options.pre_chat_message;
const { pre_chat_fields: preChatFields = [] } = options;
return {
preChatMessage,
preChatFields,
};
},
shouldShowPreChatForm() {
const { preChatFields } = this.preChatFormOptions;
// Check if at least one enabled field in pre-chat fields
const hasEnabledFields =
preChatFields.filter(field => field.enabled).length > 0;
return this.preChatFormEnabled && hasEnabledFields;
},
},
};
@@ -1,74 +0,0 @@
import { createWrapper } from '@vue/test-utils';
import configMixin from '../configMixin';
import Vue from 'vue';
const preChatFields = [
{
label: 'Email Id',
name: 'emailAddress',
type: 'email',
field_type: 'standard',
required: false,
enabled: false,
},
{
label: 'Full name',
name: 'fullName',
type: 'text',
field_type: 'standard',
required: true,
enabled: true,
},
];
global.chatwootWebChannel = {
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: [
'emoji_picker',
'attachments',
'end_conversation',
'use_inbox_avatar_for_bot',
],
preChatFormOptions: { pre_chat_fields: preChatFields, pre_chat_message: '' },
preChatFormEnabled: true,
};
describe('configMixin', () => {
test('returns config', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [configMixin],
};
const Constructor = Vue.extend(Component);
const vm = new Constructor().$mount();
const wrapper = createWrapper(vm);
expect(wrapper.vm.hasEmojiPickerEnabled).toBe(true);
expect(wrapper.vm.hasEndConversationEnabled).toBe(true);
expect(wrapper.vm.hasAttachmentsEnabled).toBe(true);
expect(wrapper.vm.hasAConnectedAgentBot).toBe(true);
expect(wrapper.vm.useInboxAvatarForBot).toBe(true);
expect(wrapper.vm.inboxAvatarUrl).toBe('https://test.url');
expect(wrapper.vm.channelConfig).toEqual({
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: [
'emoji_picker',
'attachments',
'end_conversation',
'use_inbox_avatar_for_bot',
],
preChatFormOptions: {
pre_chat_message: '',
pre_chat_fields: preChatFields,
},
preChatFormEnabled: true,
});
expect(wrapper.vm.preChatFormOptions).toEqual({
preChatMessage: '',
preChatFields: preChatFields,
});
expect(wrapper.vm.preChatFormEnabled).toEqual(true);
expect(wrapper.vm.shouldShowPreChatForm).toEqual(true);
});
});
+8 -2
View File
@@ -6,7 +6,7 @@ import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoad
import { mapGetters } from 'vuex';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import routerMixin from 'widget/mixins/routerMixin';
import configMixin from 'widget/mixins/configMixin';
import { useConfig } from 'widget/composables/useConfig';
export default {
name: 'Home',
@@ -15,7 +15,13 @@ export default {
TeamAvailability,
ArticleCardSkeletonLoader,
},
mixins: [configMixin, routerMixin, darkModeMixin],
mixins: [routerMixin, darkModeMixin],
setup() {
const { preChatFormEnabled } = useConfig();
return {
preChatFormEnabled,
};
},
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',

Some files were not shown because too many files have changed in this diff Show More