Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f3da3f814 | ||
|
|
675bb3b744 | ||
|
|
bff19eb2e6 | ||
|
|
4f7d1a67cf | ||
|
|
1336b0c750 | ||
|
|
84445e0d1e | ||
|
|
d9cd8b8261 | ||
|
|
59766ebc95 | ||
|
|
d1d958601d | ||
|
|
46beab047d | ||
|
|
b832fdc329 | ||
|
|
00ef9c475f | ||
|
|
af90f21cfd | ||
|
|
00da2ac847 | ||
|
|
5c957054fe | ||
|
|
df26e940da | ||
|
|
05c18de547 | ||
|
|
e54d426577 | ||
|
|
e6185536a8 | ||
|
|
9d4ce3964b | ||
|
|
5792542ee2 | ||
|
|
68d20a854e | ||
|
|
23e1d614bf | ||
|
|
a8ac78cd8e | ||
|
|
808a27c793 | ||
|
|
f57d0f9da9 | ||
|
|
db13956f81 | ||
|
|
47074e8a82 | ||
|
|
b8c237ca58 | ||
|
|
b6f73221b9 | ||
|
|
d974be86ee | ||
|
|
4582794ad4 | ||
|
|
f906b9a775 | ||
|
|
df373bec62 | ||
|
|
d7b564ea9f | ||
|
|
f956530b36 | ||
|
|
803471e42f | ||
|
|
8bdb1a2ce0 | ||
|
|
b0114dee42 | ||
|
|
ae35b47ae3 | ||
|
|
a8e1fba1bd | ||
|
|
ea86184675 | ||
|
|
6ec5bbc2c7 | ||
|
|
b474929f5e | ||
|
|
32f25e3bd7 | ||
|
|
d51e3ad5bf | ||
|
|
2dd2cfeb87 | ||
|
|
bd7e1fba3f | ||
|
|
bece33792e | ||
|
|
ffb687d12f | ||
|
|
980f4da669 | ||
|
|
e3eca47c31 | ||
|
|
6c682a6869 | ||
|
|
b53ce3fa79 | ||
|
|
41e27e95b4 | ||
|
|
b31be37dc2 |
+1
-1
@@ -555,7 +555,7 @@ GEM
|
||||
rack (2.2.9)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.4.0)
|
||||
rack-contrib (2.5.0)
|
||||
rack (< 4)
|
||||
rack-cors (2.0.0)
|
||||
rack (>= 2.0.0)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
include GoogleConcern
|
||||
before_action :check_authorization
|
||||
|
||||
def create
|
||||
email = params[:authorization][:email]
|
||||
redirect_url = google_client.auth_code.authorize_url(
|
||||
{
|
||||
redirect_uri: "#{base_url}/google/callback",
|
||||
scope: 'email profile https://mail.google.com/',
|
||||
response_type: 'code',
|
||||
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
|
||||
access_type: 'offline', # the default is 'online'
|
||||
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
||||
}
|
||||
)
|
||||
|
||||
if redirect_url
|
||||
cache_key = "google::#{email.downcase}"
|
||||
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
|
||||
render json: { success: true, url: redirect_url }
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
end
|
||||
@@ -12,8 +12,8 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
|
||||
}
|
||||
)
|
||||
if redirect_url
|
||||
email = email.downcase
|
||||
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
|
||||
cache_key = "microsoft::#{email.downcase}"
|
||||
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
|
||||
render json: { success: true, url: redirect_url }
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module GoogleConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def google_client
|
||||
app_id = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
||||
app_secret = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_SECRET', nil)
|
||||
|
||||
::OAuth2::Client.new(app_id, app_secret, {
|
||||
site: 'https://oauth2.googleapis.com',
|
||||
authorize_url: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_url: 'https://accounts.google.com/o/oauth2/token'
|
||||
})
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
end
|
||||
@@ -15,10 +15,6 @@ module MicrosoftConcern
|
||||
|
||||
private
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
|
||||
end
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
class Google::CallbacksController < OauthCallbackController
|
||||
include GoogleConcern
|
||||
|
||||
private
|
||||
|
||||
def provider_name
|
||||
'google'
|
||||
end
|
||||
|
||||
def imap_address
|
||||
'imap.gmail.com'
|
||||
end
|
||||
|
||||
def oauth_client
|
||||
# from GoogleConcern
|
||||
google_client
|
||||
end
|
||||
end
|
||||
@@ -1,90 +1,17 @@
|
||||
class Microsoft::CallbacksController < ApplicationController
|
||||
class Microsoft::CallbacksController < OauthCallbackController
|
||||
include MicrosoftConcern
|
||||
|
||||
def show
|
||||
@response = microsoft_client.auth_code.get_token(
|
||||
oauth_code,
|
||||
redirect_uri: "#{base_url}/microsoft/callback"
|
||||
)
|
||||
|
||||
inbox, already_exists = find_or_create_inbox
|
||||
::Redis::Alfred.delete(users_data['email'].downcase)
|
||||
|
||||
if already_exists
|
||||
redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||
else
|
||||
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
end
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_to '/'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oauth_code
|
||||
params[:code]
|
||||
def oauth_client
|
||||
microsoft_client
|
||||
end
|
||||
|
||||
def users_data
|
||||
decoded_token = JWT.decode parsed_body[:id_token], nil, false
|
||||
decoded_token[0]
|
||||
def provider_name
|
||||
'microsoft'
|
||||
end
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= @response.response.parsed
|
||||
end
|
||||
|
||||
def account_id
|
||||
::Redis::Alfred.get(users_data['email'].downcase)
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
end
|
||||
|
||||
def find_or_create_inbox
|
||||
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
# we need this value to know where to redirect on sucessful processing of the callback
|
||||
channel_exists = channel_email.present?
|
||||
|
||||
channel_email ||= create_microsoft_channel_with_inbox
|
||||
update_microsoft_channel(channel_email)
|
||||
|
||||
# reauthorize channel, this code path only triggers when microsoft auth is successful
|
||||
# reauthorized will also update cache keys for the associated inbox
|
||||
channel_email.reauthorized!
|
||||
|
||||
[channel_email.inbox, channel_exists]
|
||||
end
|
||||
|
||||
# Fallback name, for when name field is missing from users_data
|
||||
def fallback_name
|
||||
users_data['email'].split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
def create_microsoft_channel_with_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||
account.inboxes.create!(
|
||||
account: account,
|
||||
channel: channel_email,
|
||||
name: users_data['name'] || fallback_name
|
||||
)
|
||||
channel_email
|
||||
end
|
||||
end
|
||||
|
||||
def update_microsoft_channel(channel_email)
|
||||
channel_email.update!({
|
||||
imap_login: users_data['email'], imap_address: 'outlook.office365.com',
|
||||
imap_port: '993', imap_enabled: true,
|
||||
provider: 'microsoft',
|
||||
provider_config: {
|
||||
access_token: parsed_body['access_token'],
|
||||
refresh_token: parsed_body['refresh_token'],
|
||||
expires_on: (Time.current.utc + 1.hour).to_s
|
||||
}
|
||||
})
|
||||
def imap_address
|
||||
'outlook.office365.com'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
class OauthCallbackController < ApplicationController
|
||||
def show
|
||||
@response = oauth_client.auth_code.get_token(
|
||||
oauth_code,
|
||||
redirect_uri: "#{base_url}/#{provider_name}/callback"
|
||||
)
|
||||
|
||||
handle_response
|
||||
::Redis::Alfred.delete(cache_key)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_to '/'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_response
|
||||
inbox, already_exists = find_or_create_inbox
|
||||
|
||||
if already_exists
|
||||
redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||
else
|
||||
redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_inbox
|
||||
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
# we need this value to know where to redirect on sucessful processing of the callback
|
||||
channel_exists = channel_email.present?
|
||||
|
||||
channel_email ||= create_channel_with_inbox
|
||||
update_channel(channel_email)
|
||||
|
||||
# reauthorize channel, this code path only triggers when microsoft auth is successful
|
||||
# reauthorized will also update cache keys for the associated inbox
|
||||
channel_email.reauthorized!
|
||||
|
||||
[channel_email.inbox, channel_exists]
|
||||
end
|
||||
|
||||
def update_channel(channel_email)
|
||||
channel_email.update!({
|
||||
imap_login: users_data['email'], imap_address: imap_address,
|
||||
imap_port: '993', imap_enabled: true,
|
||||
provider: provider_name,
|
||||
provider_config: {
|
||||
access_token: parsed_body['access_token'],
|
||||
refresh_token: parsed_body['refresh_token'],
|
||||
expires_on: (Time.current.utc + 1.hour).to_s
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
def provider_name
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def oauth_client
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"#{provider_name}::#{users_data['email'].downcase}"
|
||||
end
|
||||
|
||||
def create_channel_with_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||
account.inboxes.create!(
|
||||
account: account,
|
||||
channel: channel_email,
|
||||
name: users_data['name'] || fallback_name
|
||||
)
|
||||
channel_email
|
||||
end
|
||||
end
|
||||
|
||||
def users_data
|
||||
decoded_token = JWT.decode parsed_body[:id_token], nil, false
|
||||
decoded_token[0]
|
||||
end
|
||||
|
||||
def account_id
|
||||
::Redis::Alfred.get(cache_key)
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
end
|
||||
|
||||
# Fallback name, for when name field is missing from users_data
|
||||
def fallback_name
|
||||
users_data['email'].split('@').first.parameterize.titleize
|
||||
end
|
||||
|
||||
def oauth_code
|
||||
params[:code]
|
||||
end
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= @response.response.parsed
|
||||
end
|
||||
end
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import router from '../dashboard/routes';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification.vue';
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
} from './helper/pushHelper';
|
||||
import ReconnectService from 'dashboard/helper/ReconnectService';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@@ -64,6 +66,7 @@ export default {
|
||||
return {
|
||||
showAddAccountModal: false,
|
||||
latestChatwootVersion: null,
|
||||
reconnectService: null,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -102,6 +105,11 @@ export default {
|
||||
this.listenToThemeChanges();
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.reconnectService) {
|
||||
this.reconnectService.disconnect();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeColorTheme() {
|
||||
setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
@@ -125,6 +133,7 @@ export default {
|
||||
this.updateRTLDirectionView(locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
vueActionCable.init(pubsubToken);
|
||||
this.reconnectService = new ReconnectService(this.$store, router);
|
||||
|
||||
verifyServiceWorkerExistence(registration =>
|
||||
registration.pushManager.getSubscription().then(subscription => {
|
||||
|
||||
@@ -9,6 +9,13 @@ class AccountAPI extends ApiClient {
|
||||
createAccount(data) {
|
||||
return axios.post(`${this.apiVersion}/accounts`, data);
|
||||
}
|
||||
|
||||
async getCacheKeys() {
|
||||
const response = await axios.get(
|
||||
`/api/v1/accounts/${this.accountIdFromRoute}/cache_keys`
|
||||
);
|
||||
return response.data.cache_keys;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccountAPI();
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class MicrosoftClient extends ApiClient {
|
||||
constructor() {
|
||||
super('google', { accountScoped: true });
|
||||
}
|
||||
|
||||
generateAuthorization(payload) {
|
||||
return axios.post(`${this.url}/authorization`, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default new MicrosoftClient();
|
||||
@@ -15,6 +15,7 @@ class ConversationApi extends ApiClient {
|
||||
teamId,
|
||||
conversationType,
|
||||
sortBy,
|
||||
updatedWithin,
|
||||
}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
@@ -26,6 +27,7 @@ class ConversationApi extends ApiClient {
|
||||
labels,
|
||||
conversation_type: conversationType,
|
||||
sort_by: sortBy,
|
||||
updated_within: updatedWithin,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ describe('#ConversationAPI', () => {
|
||||
page: 1,
|
||||
labels: [],
|
||||
teamId: 1,
|
||||
updatedWithin: 20,
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/conversations', {
|
||||
params: {
|
||||
@@ -55,6 +56,7 @@ describe('#ConversationAPI', () => {
|
||||
assignee_type: 'me',
|
||||
page: 1,
|
||||
labels: [],
|
||||
updated_within: 20,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<button
|
||||
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
|
||||
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
|
||||
|
||||
@@ -268,6 +268,7 @@ export default {
|
||||
chatLists: 'getAllConversations',
|
||||
mineChatsList: 'getMineChats',
|
||||
allChatList: 'getAllStatusChats',
|
||||
chatListFilters: 'getChatListFilters',
|
||||
unAssignedChatsList: 'getUnAssignedChats',
|
||||
chatListLoading: 'getChatListLoadingStatus',
|
||||
currentUserID: 'getCurrentUserID',
|
||||
@@ -293,13 +294,6 @@ export default {
|
||||
hasAppliedFiltersOrActiveFolders() {
|
||||
return this.hasAppliedFilters || this.hasActiveFolders;
|
||||
},
|
||||
savedFoldersValue() {
|
||||
if (this.hasActiveFolders) {
|
||||
const payload = this.activeFolder.query;
|
||||
this.fetchSavedFilteredConversations(payload);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
showEndOfListMessage() {
|
||||
return (
|
||||
this.conversationList.length &&
|
||||
@@ -375,7 +369,6 @@ export default {
|
||||
labels: this.label ? [this.label] : undefined,
|
||||
teamId: this.teamId || undefined,
|
||||
conversationType: this.conversationType || undefined,
|
||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||
};
|
||||
},
|
||||
conversationListPagination() {
|
||||
@@ -488,7 +481,13 @@ export default {
|
||||
this.resetAndFetchData();
|
||||
this.updateVirtualListProps('conversationType', this.conversationType);
|
||||
},
|
||||
activeFolder() {
|
||||
activeFolder(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$store.dispatch(
|
||||
'customViews/setActiveConversationFolder',
|
||||
newVal || null
|
||||
);
|
||||
}
|
||||
this.resetAndFetchData();
|
||||
this.updateVirtualListProps('foldersId', this.foldersId);
|
||||
},
|
||||
@@ -498,8 +497,14 @@ export default {
|
||||
showAssigneeInConversationCard(newVal) {
|
||||
this.updateVirtualListProps('showAssignee', newVal);
|
||||
},
|
||||
conversationFilters(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$store.dispatch('updateChatListFilters', newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setChatListFilters', this.conversationFilters);
|
||||
this.setFiltersFromUISettings();
|
||||
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
|
||||
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
||||
@@ -509,14 +514,14 @@ export default {
|
||||
this.$store.dispatch('campaigns/get');
|
||||
}
|
||||
|
||||
bus.$on('fetch_conversation_stats', () => {
|
||||
this.$emitter.on('fetch_conversation_stats', () => {
|
||||
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||
});
|
||||
|
||||
bus.$on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
methods: {
|
||||
updateVirtualListProps(key, value) {
|
||||
@@ -695,8 +700,9 @@ export default {
|
||||
this.fetchConversations();
|
||||
},
|
||||
fetchConversations() {
|
||||
this.$store.dispatch('updateChatListFilters', this.conversationFilters);
|
||||
this.$store
|
||||
.dispatch('fetchAllConversations', this.conversationFilters)
|
||||
.dispatch('fetchAllConversations')
|
||||
.then(this.emitConversationLoaded);
|
||||
},
|
||||
loadMoreConversations() {
|
||||
@@ -736,7 +742,7 @@ export default {
|
||||
updateAssigneeTab(selectedTab) {
|
||||
if (this.activeAssigneeTab !== selectedTab) {
|
||||
this.resetBulkActions();
|
||||
bus.$emit('clearSearchInput');
|
||||
this.$emitter.emit('clearSearchInput');
|
||||
this.activeAssigneeTab = selectedTab;
|
||||
if (!this.currentPage) {
|
||||
this.fetchConversations();
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
<script>
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
script: {
|
||||
type: String,
|
||||
@@ -59,7 +61,7 @@ export default {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.script);
|
||||
bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -270,10 +270,10 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.editedValue = this.formattedValue;
|
||||
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
||||
this.$emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
||||
this.$emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
|
||||
},
|
||||
methods: {
|
||||
onFocusAttribute(focusAttributeKey) {
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
<script>
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
@@ -35,7 +37,7 @@ export default {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.value);
|
||||
bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
toggleMasked() {
|
||||
this.masked = !this.masked;
|
||||
|
||||
@@ -1,26 +1,133 @@
|
||||
<script setup>
|
||||
import { ref, computed, onBeforeUnmount } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useRoute } from 'dashboard/composables/route';
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
isNotificationRoute,
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const RECONNECTED_BANNER_TIMEOUT = 2000;
|
||||
|
||||
const showNotification = ref(!navigator.onLine);
|
||||
const isDisconnected = ref(false);
|
||||
const isReconnecting = ref(false);
|
||||
const isReconnected = ref(false);
|
||||
let reconnectTimeout = null;
|
||||
|
||||
const bannerText = computed(() => {
|
||||
if (isReconnecting.value) return t('NETWORK.NOTIFICATION.RECONNECTING');
|
||||
if (isReconnected.value) return t('NETWORK.NOTIFICATION.RECONNECT_SUCCESS');
|
||||
return t('NETWORK.NOTIFICATION.OFFLINE');
|
||||
});
|
||||
|
||||
const iconName = computed(() => (isReconnected.value ? 'wifi' : 'wifi-off'));
|
||||
const canRefresh = computed(
|
||||
() => !isReconnecting.value && !isReconnected.value
|
||||
);
|
||||
|
||||
const refreshPage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const closeNotification = () => {
|
||||
showNotification.value = false;
|
||||
isReconnected.value = false;
|
||||
clearTimeout(reconnectTimeout);
|
||||
};
|
||||
|
||||
const isInAnyOfTheRoutes = routeName => {
|
||||
return (
|
||||
isAConversationRoute(routeName, true) ||
|
||||
isAInboxViewRoute(routeName, true) ||
|
||||
isNotificationRoute(routeName, true)
|
||||
);
|
||||
};
|
||||
|
||||
const updateWebsocketStatus = () => {
|
||||
isDisconnected.value = true;
|
||||
showNotification.value = true;
|
||||
};
|
||||
|
||||
const handleReconnectionCompleted = () => {
|
||||
isDisconnected.value = false;
|
||||
isReconnecting.value = false;
|
||||
isReconnected.value = true;
|
||||
showNotification.value = true;
|
||||
reconnectTimeout = setTimeout(closeNotification, RECONNECTED_BANNER_TIMEOUT);
|
||||
};
|
||||
|
||||
const handleReconnecting = () => {
|
||||
if (isInAnyOfTheRoutes(route.name)) {
|
||||
isReconnecting.value = true;
|
||||
isReconnected.value = false;
|
||||
showNotification.value = true;
|
||||
} else {
|
||||
handleReconnectionCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
const updateOnlineStatus = event => {
|
||||
// Case: Websocket is not disconnected
|
||||
// If the app goes offline, show the notification
|
||||
// If the app goes online, close the notification
|
||||
|
||||
// Case: Websocket is disconnected
|
||||
// If the app goes offline, show the notification
|
||||
// If the app goes online but the websocket is disconnected, don't close the notification
|
||||
// If the app goes online and the websocket is not disconnected, close the notification
|
||||
|
||||
if (event.type === 'offline') {
|
||||
showNotification.value = true;
|
||||
} else if (event.type === 'online' && !isDisconnected.value) {
|
||||
handleReconnectionCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
useEventListener('online', updateOnlineStatus);
|
||||
useEventListener('offline', updateOnlineStatus);
|
||||
useEmitter(BUS_EVENTS.WEBSOCKET_DISCONNECT, updateWebsocketStatus);
|
||||
useEmitter(
|
||||
BUS_EVENTS.WEBSOCKET_RECONNECT_COMPLETED,
|
||||
handleReconnectionCompleted
|
||||
);
|
||||
useEmitter(BUS_EVENTS.WEBSOCKET_RECONNECT, handleReconnecting);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(reconnectTimeout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="network-notification-fade" tag="div">
|
||||
<div v-show="showNotification" class="fixed top-4 left-2 z-50 group">
|
||||
<div v-show="showNotification" class="fixed z-50 top-4 left-2 group">
|
||||
<div
|
||||
class="flex items-center justify-between py-1 px-2 w-full rounded-lg shadow-lg bg-yellow-200 dark:bg-yellow-700 relative"
|
||||
class="relative flex items-center justify-between w-full px-2 py-1 bg-yellow-200 rounded-lg shadow-lg dark:bg-yellow-700"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="wifi-off"
|
||||
:icon="iconName"
|
||||
class="text-yellow-700/50 dark:text-yellow-50"
|
||||
size="18"
|
||||
/>
|
||||
<span
|
||||
class="text-xs tracking-wide font-medium px-2 text-yellow-700/70 dark:text-yellow-50"
|
||||
class="px-2 text-xs font-medium tracking-wide text-yellow-700/70 dark:text-yellow-50"
|
||||
>
|
||||
{{ $t('NETWORK.NOTIFICATION.OFFLINE') }}
|
||||
{{ bannerText }}
|
||||
</span>
|
||||
<woot-button
|
||||
v-if="canRefresh"
|
||||
:title="$t('NETWORK.BUTTON.REFRESH')"
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="warning"
|
||||
icon="arrow-clockwise"
|
||||
class="visible transition-all duration-500 ease-in-out ml-1"
|
||||
@click="refreshPage"
|
||||
/>
|
||||
<woot-button
|
||||
@@ -34,55 +141,3 @@
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showNotification: !navigator.onLine,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('offline', this.updateOnlineStatus);
|
||||
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
|
||||
// TODO: Remove this after completing the conversation list refetching
|
||||
// TODO: DIRTY FIX : CLEAN UP THIS WITH PROPER FIX, DELAYING THE RECONNECT FOR NOW
|
||||
// THE CABLE IS FIRING IS VERY COMMON AND THUS INTERFERING USER EXPERIENCE
|
||||
setTimeout(() => {
|
||||
this.updateOnlineStatus({ type: 'offline' });
|
||||
}, 4000);
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('offline', this.updateOnlineStatus);
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshPage() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
closeNotification() {
|
||||
this.showNotification = false;
|
||||
},
|
||||
|
||||
updateOnlineStatus(event) {
|
||||
if (event.type === 'offline') {
|
||||
this.showNotification = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onMenuItemClick() {
|
||||
bus.$emit(BUS_EVENTS.TOGGLE_SIDEMENU);
|
||||
this.$emitter.emit(BUS_EVENTS.TOGGLE_SIDEMENU);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
|
||||
<script>
|
||||
import WootSnackbar from './Snackbar.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootSnackbar,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
duration: {
|
||||
type: Number,
|
||||
@@ -34,10 +36,10 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
bus.$on('newToastMessage', this.onNewToastMessage);
|
||||
this.$emitter.on('newToastMessage', this.onNewToastMessage);
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off('newToastMessage', this.onNewToastMessage);
|
||||
this.$emitter.off('newToastMessage', this.onNewToastMessage);
|
||||
},
|
||||
methods: {
|
||||
onNewToastMessage(message, action) {
|
||||
|
||||
@@ -128,12 +128,12 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
bus.$on(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
this.$emitter.on(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
this.$emitter.on(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
this.$emitter.off(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
this.$emitter.off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
methods: {
|
||||
getKeyboardEvents() {
|
||||
|
||||
+3
-3
@@ -65,11 +65,11 @@
|
||||
</div>
|
||||
<span
|
||||
v-if="warningIcon"
|
||||
class="inline-flex rounded-sm mr-1 bg-slate-100"
|
||||
class="inline-flex mr-1 bg-red-50 dark:bg-red-900 p-0.5 rounded-sm"
|
||||
>
|
||||
<fluent-icon
|
||||
v-tooltip.top-end="$t('SIDEBAR.FACEBOOK_REAUTHORIZE')"
|
||||
class="text-xxs"
|
||||
v-tooltip.top-end="$t('SIDEBAR.REAUTHORIZE')"
|
||||
class="text-xxs text-red-500 dark:text-red-300"
|
||||
:icon="warningIcon"
|
||||
size="12"
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
setYear,
|
||||
isAfter,
|
||||
} from 'date-fns';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import DatePickerButton from './components/DatePickerButton.vue';
|
||||
import CalendarDateInput from './components/CalendarDateInput.vue';
|
||||
import CalendarDateRange from './components/CalendarDateRange.vue';
|
||||
@@ -185,7 +185,7 @@ const updateManualInput = (newDate, calendarType) => {
|
||||
};
|
||||
|
||||
const handleManualInputError = message => {
|
||||
bus.$emit('newToastMessage', message);
|
||||
useAlert(message);
|
||||
};
|
||||
|
||||
const resetDatePicker = () => {
|
||||
@@ -201,7 +201,7 @@ const resetDatePicker = () => {
|
||||
|
||||
const emitDateRange = () => {
|
||||
if (!isValid(selectedStartDate.value) || !isValid(selectedEndDate.value)) {
|
||||
bus.$emit('newToastMessage', 'Please select a valid time range');
|
||||
useAlert('Please select a valid time range');
|
||||
} else {
|
||||
showDatePicker.value = false;
|
||||
emit('dateRangeChanged', [selectedStartDate.value, selectedEndDate.value]);
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
bus.$on(CMD_AI_ASSIST, this.onAIAssist);
|
||||
this.$emitter.on(CMD_AI_ASSIST, this.onAIAssist);
|
||||
this.initialMessage = this.draftMessage;
|
||||
},
|
||||
|
||||
|
||||
@@ -352,10 +352,16 @@ export default {
|
||||
// Components using this
|
||||
// 1. SearchPopover.vue
|
||||
|
||||
bus.$on(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
|
||||
this.$emitter.on(
|
||||
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
||||
this.insertContentIntoEditor
|
||||
);
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
|
||||
this.$emitter.off(
|
||||
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
||||
this.insertContentIntoEditor
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
reloadState(content = this.value) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
|
||||
<li
|
||||
v-if="shouldRenderMessage"
|
||||
:id="`message${data.id}`"
|
||||
:class="[alignBubble, 'group']"
|
||||
>
|
||||
<div :class="wrapClass">
|
||||
<div
|
||||
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
|
||||
@@ -121,7 +125,10 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
||||
<div
|
||||
v-if="shouldShowContextMenu"
|
||||
class="context-menu-wrap invisible group-hover:visible"
|
||||
>
|
||||
<context-menu
|
||||
v-if="isBubble && !isMessageDeleted"
|
||||
:context-menu-position="contextMenuPosition"
|
||||
@@ -473,11 +480,11 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.hasMediaLoadError = false;
|
||||
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
this.$emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
this.setupHighlightTimer();
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
this.$emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
|
||||
clearTimeout(this.higlightTimeout);
|
||||
},
|
||||
methods: {
|
||||
@@ -531,7 +538,7 @@ export default {
|
||||
const { conversation_id: conversationId, id: replyTo } = this.data;
|
||||
|
||||
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
|
||||
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
|
||||
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
|
||||
},
|
||||
setupHighlightTimer() {
|
||||
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
|
||||
|
||||
@@ -324,12 +324,12 @@ export default {
|
||||
},
|
||||
|
||||
created() {
|
||||
bus.$on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||
this.$emitter.on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||
// when a new message comes in, we refetch the label suggestions
|
||||
bus.$on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
|
||||
this.$emitter.on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
|
||||
// when a message is sent we set the flag to true this hides the label suggestions,
|
||||
// until the chat is changed and the flag is reset in the watch for currentChat
|
||||
bus.$on(BUS_EVENTS.MESSAGE_SENT, () => {
|
||||
this.$emitter.on(BUS_EVENTS.MESSAGE_SENT, () => {
|
||||
this.messageSentSinceOpened = true;
|
||||
});
|
||||
},
|
||||
@@ -396,7 +396,7 @@ export default {
|
||||
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
|
||||
},
|
||||
removeBusListeners() {
|
||||
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||
this.$emitter.off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||
},
|
||||
onScrollToMessage({ messageId = '' } = {}) {
|
||||
this.$nextTick(() => {
|
||||
@@ -514,7 +514,7 @@ export default {
|
||||
} else {
|
||||
this.hasUserScrolled = true;
|
||||
}
|
||||
bus.$emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
|
||||
this.$emitter.emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
|
||||
this.fetchPreviousMessages(e.target.scrollTop);
|
||||
},
|
||||
|
||||
|
||||
@@ -61,14 +61,14 @@ export default {
|
||||
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(CMD_MUTE_CONVERSATION, this.mute);
|
||||
bus.$on(CMD_UNMUTE_CONVERSATION, this.unmute);
|
||||
bus.$on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
||||
this.$emitter.on(CMD_MUTE_CONVERSATION, this.mute);
|
||||
this.$emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
|
||||
this.$emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(CMD_MUTE_CONVERSATION, this.mute);
|
||||
bus.$off(CMD_UNMUTE_CONVERSATION, this.unmute);
|
||||
bus.$off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
||||
this.$emitter.off(CMD_MUTE_CONVERSATION, this.mute);
|
||||
this.$emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
|
||||
this.$emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
|
||||
},
|
||||
methods: {
|
||||
mute() {
|
||||
|
||||
@@ -596,12 +596,15 @@ export default {
|
||||
);
|
||||
|
||||
this.fetchAndSetReplyTo();
|
||||
bus.$on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
||||
this.$emitter.on(
|
||||
BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE,
|
||||
this.fetchAndSetReplyTo
|
||||
);
|
||||
|
||||
// A hacky fix to solve the drag and drop
|
||||
// Is showing on top of new conversation modal drag and drop
|
||||
// TODO need to find a better solution
|
||||
bus.$on(
|
||||
this.$emitter.on(
|
||||
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||
this.onNewConversationModalActive
|
||||
);
|
||||
@@ -609,10 +612,13 @@ export default {
|
||||
destroyed() {
|
||||
document.removeEventListener('paste', this.onPaste);
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
bus.$off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
|
||||
this.$emitter.off(
|
||||
BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE,
|
||||
this.fetchAndSetReplyTo
|
||||
);
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(
|
||||
this.$emitter.off(
|
||||
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||
this.onNewConversationModalActive
|
||||
);
|
||||
@@ -625,7 +631,7 @@ export default {
|
||||
const lines = title.split('\n');
|
||||
const nonEmptyLines = lines.filter(line => line.trim() !== '');
|
||||
const filteredMarkdown = nonEmptyLines.join(' ');
|
||||
bus.$emit(
|
||||
this.$emitter.emit(
|
||||
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
||||
`[${filteredMarkdown}](${url})`
|
||||
);
|
||||
@@ -867,8 +873,8 @@ export default {
|
||||
'createPendingMessageAndSend',
|
||||
messagePayload
|
||||
);
|
||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
bus.$emit(BUS_EVENTS.MESSAGE_SENT);
|
||||
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
this.$emitter.emit(BUS_EVENTS.MESSAGE_SENT);
|
||||
this.removeFromDraft();
|
||||
this.sendMessageAnalyticsData(messagePayload.private);
|
||||
} catch (error) {
|
||||
@@ -1194,7 +1200,7 @@ export default {
|
||||
resetReplyToMessage() {
|
||||
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
|
||||
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
|
||||
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
|
||||
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
|
||||
},
|
||||
onNewConversationModalActive(isActive) {
|
||||
// Issue is if the new conversation modal is open and we drag and drop the file
|
||||
|
||||
@@ -47,7 +47,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
scrollToMessage() {
|
||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE, { messageId: this.message.id });
|
||||
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
|
||||
messageId: this.message.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+18
-6
@@ -167,17 +167,29 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(CMD_BULK_ACTION_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
bus.$on(CMD_BULK_ACTION_REOPEN_CONVERSATION, this.onCmdReopenConversation);
|
||||
bus.$on(
|
||||
this.$emitter.on(
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
this.onCmdSnoozeConversation
|
||||
);
|
||||
this.$emitter.on(
|
||||
CMD_BULK_ACTION_REOPEN_CONVERSATION,
|
||||
this.onCmdReopenConversation
|
||||
);
|
||||
this.$emitter.on(
|
||||
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
|
||||
this.onCmdResolveConversation
|
||||
);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(CMD_BULK_ACTION_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
bus.$off(CMD_BULK_ACTION_REOPEN_CONVERSATION, this.onCmdReopenConversation);
|
||||
bus.$off(
|
||||
this.$emitter.off(
|
||||
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
|
||||
this.onCmdSnoozeConversation
|
||||
);
|
||||
this.$emitter.off(
|
||||
CMD_BULK_ACTION_REOPEN_CONVERSATION,
|
||||
this.onCmdReopenConversation
|
||||
);
|
||||
this.$emitter.off(
|
||||
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
|
||||
this.onCmdResolveConversation
|
||||
);
|
||||
|
||||
@@ -2,12 +2,21 @@ import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
import Button from 'dashboard/components/buttons/Button';
|
||||
import i18n from 'dashboard/i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
import MoreActions from '../MoreActions';
|
||||
|
||||
jest.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
@@ -16,6 +25,12 @@ localVue.use(VTooltip);
|
||||
localVue.component('fluent-icon', FluentIcon);
|
||||
localVue.component('woot-button', Button);
|
||||
|
||||
localVue.prototype.$emitter = {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
};
|
||||
|
||||
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
|
||||
|
||||
describe('MoveActions', () => {
|
||||
@@ -29,12 +44,6 @@ describe('MoveActions', () => {
|
||||
let moreActions = null;
|
||||
|
||||
beforeEach(() => {
|
||||
window.bus = {
|
||||
$emit: jest.fn(),
|
||||
$on: jest.fn(),
|
||||
$off: jest.fn(),
|
||||
};
|
||||
|
||||
state = {
|
||||
authenticated: true,
|
||||
currentChat,
|
||||
@@ -76,7 +85,7 @@ describe('MoveActions', () => {
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
expect(emitter.emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This contact is blocked successfully. You will not be notified of any future conversations.',
|
||||
undefined
|
||||
@@ -102,7 +111,7 @@ describe('MoveActions', () => {
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
expect(emitter.emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This contact is unblocked successfully.',
|
||||
undefined
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
// this will automatically add event listeners to the emitter
|
||||
// and remove them when the component is destroyed
|
||||
const useEmitter = (eventName, callback) => {
|
||||
const cleanup = () => {
|
||||
emitter.off(eventName, callback);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(eventName, callback);
|
||||
});
|
||||
|
||||
onBeforeUnmount(cleanup);
|
||||
|
||||
return cleanup;
|
||||
};
|
||||
|
||||
export { useEmitter };
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export const useTrack = () => {
|
||||
const vm = getCurrentInstance();
|
||||
@@ -8,5 +9,5 @@ export const useTrack = () => {
|
||||
};
|
||||
|
||||
export function useAlert(message, action) {
|
||||
bus.$emit('newToastMessage', message, action);
|
||||
emitter.emit('newToastMessage', message, action);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { useEmitter } from '../emitter';
|
||||
|
||||
jest.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useEmitter', () => {
|
||||
let wrapper;
|
||||
const eventName = 'my-event';
|
||||
const callback = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount({
|
||||
template: `
|
||||
<div>
|
||||
Hello world
|
||||
</div>
|
||||
`,
|
||||
setup() {
|
||||
return {
|
||||
cleanup: useEmitter(eventName, callback),
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should add an event listener on mount', () => {
|
||||
expect(emitter.on).toHaveBeenCalledWith(eventName, callback);
|
||||
});
|
||||
|
||||
it('should remove the event listener when the component is unmounted', () => {
|
||||
wrapper.destroy();
|
||||
expect(emitter.off).toHaveBeenCalledWith(eventName, callback);
|
||||
});
|
||||
|
||||
it('should return the cleanup function', () => {
|
||||
const cleanup = wrapper.vm.cleanup;
|
||||
expect(typeof cleanup).toBe('function');
|
||||
cleanup();
|
||||
expect(emitter.off).toHaveBeenCalledWith(eventName, callback);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { differenceInSeconds } from 'date-fns';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
isNotificationRoute,
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
|
||||
const MAX_DISCONNECT_SECONDS = 10800;
|
||||
|
||||
class ReconnectService {
|
||||
constructor(store, router) {
|
||||
this.store = store;
|
||||
this.router = router;
|
||||
this.disconnectTime = null;
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
disconnect = () => this.removeEventListeners();
|
||||
|
||||
setupEventListeners = () => {
|
||||
window.addEventListener('online', this.handleOnlineEvent);
|
||||
emitter.on(BUS_EVENTS.WEBSOCKET_RECONNECT, this.onReconnect);
|
||||
emitter.on(BUS_EVENTS.WEBSOCKET_DISCONNECT, this.onDisconnect);
|
||||
};
|
||||
|
||||
removeEventListeners = () => {
|
||||
window.removeEventListener('online', this.handleOnlineEvent);
|
||||
emitter.off(BUS_EVENTS.WEBSOCKET_RECONNECT, this.onReconnect);
|
||||
emitter.off(BUS_EVENTS.WEBSOCKET_DISCONNECT, this.onDisconnect);
|
||||
};
|
||||
|
||||
getSecondsSinceDisconnect = () =>
|
||||
this.disconnectTime
|
||||
? Math.max(differenceInSeconds(new Date(), this.disconnectTime), 0)
|
||||
: 0;
|
||||
|
||||
// Force reload if the user is disconnected for more than 3 hours
|
||||
handleOnlineEvent = () => {
|
||||
if (this.getSecondsSinceDisconnect() >= MAX_DISCONNECT_SECONDS) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
fetchConversations = async () => {
|
||||
await this.store.dispatch('updateChatListFilters', {
|
||||
page: null,
|
||||
updatedWithin: this.getSecondsSinceDisconnect(),
|
||||
});
|
||||
await this.store.dispatch('fetchAllConversations');
|
||||
// Reset the updatedWithin in the store chat list filter after fetching conversations when the user is reconnected
|
||||
await this.store.dispatch('updateChatListFilters', {
|
||||
updatedWithin: null,
|
||||
});
|
||||
};
|
||||
|
||||
fetchFilteredOrSavedConversations = async queryData => {
|
||||
await this.store.dispatch('fetchFilteredConversations', {
|
||||
queryData,
|
||||
page: 1,
|
||||
});
|
||||
};
|
||||
|
||||
fetchConversationsOnReconnect = async () => {
|
||||
const {
|
||||
getAppliedConversationFiltersQuery,
|
||||
'customViews/getActiveConversationFolder': activeFolder,
|
||||
} = this.store.getters;
|
||||
const query = getAppliedConversationFiltersQuery?.payload?.length
|
||||
? getAppliedConversationFiltersQuery
|
||||
: activeFolder?.query;
|
||||
if (query) {
|
||||
await this.fetchFilteredOrSavedConversations(query);
|
||||
} else {
|
||||
await this.fetchConversations();
|
||||
}
|
||||
};
|
||||
|
||||
fetchConversationMessagesOnReconnect = async () => {
|
||||
const { conversation_id: conversationId } = this.router.currentRoute.params;
|
||||
if (conversationId) {
|
||||
await this.store.dispatch('syncActiveConversationMessages', {
|
||||
conversationId: Number(conversationId),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchNotificationsOnReconnect = async filter => {
|
||||
await this.store.dispatch('notifications/index', { ...filter, page: 1 });
|
||||
};
|
||||
|
||||
revalidateCaches = async () => {
|
||||
const { label, inbox, team } = await this.store.dispatch(
|
||||
'accounts/getCacheKeys'
|
||||
);
|
||||
await Promise.all([
|
||||
this.store.dispatch('labels/revalidate', { newKey: label }),
|
||||
this.store.dispatch('inboxes/revalidate', { newKey: inbox }),
|
||||
this.store.dispatch('teams/revalidate', { newKey: team }),
|
||||
]);
|
||||
};
|
||||
|
||||
handleRouteSpecificFetch = async () => {
|
||||
const currentRoute = this.router.currentRoute.name;
|
||||
if (isAConversationRoute(currentRoute, true)) {
|
||||
await this.fetchConversationsOnReconnect();
|
||||
await this.fetchConversationMessagesOnReconnect();
|
||||
} else if (isAInboxViewRoute(currentRoute, true)) {
|
||||
await this.fetchNotificationsOnReconnect(
|
||||
this.store.getters['notifications/getNotificationFilters']
|
||||
);
|
||||
} else if (isNotificationRoute(currentRoute)) {
|
||||
await this.fetchNotificationsOnReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
setConversationLastMessageId = async () => {
|
||||
const { conversation_id: conversationId } = this.router.currentRoute.params;
|
||||
if (conversationId) {
|
||||
await this.store.dispatch('setConversationLastMessageId', {
|
||||
conversationId: Number(conversationId),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDisconnect = () => {
|
||||
this.disconnectTime = new Date();
|
||||
this.setConversationLastMessageId();
|
||||
};
|
||||
|
||||
onReconnect = async () => {
|
||||
await this.handleRouteSpecificFetch();
|
||||
await this.revalidateCaches();
|
||||
emitter.emit(BUS_EVENTS.WEBSOCKET_RECONNECT_COMPLETED);
|
||||
};
|
||||
}
|
||||
|
||||
export default ReconnectService;
|
||||
@@ -1,6 +1,8 @@
|
||||
import AuthAPI from '../api/auth';
|
||||
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
|
||||
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
@@ -32,34 +34,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onReconnect = () => {
|
||||
this.syncActiveConversationMessages();
|
||||
emitter.emit(BUS_EVENTS.WEBSOCKET_RECONNECT);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onDisconnected = () => {
|
||||
this.setActiveConversationLastMessageId();
|
||||
};
|
||||
|
||||
setActiveConversationLastMessageId = () => {
|
||||
const {
|
||||
params: { conversation_id },
|
||||
} = this.app.$route;
|
||||
if (conversation_id) {
|
||||
this.app.$store.dispatch('setConversationLastMessageId', {
|
||||
conversationId: Number(conversation_id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
syncActiveConversationMessages = () => {
|
||||
const {
|
||||
params: { conversation_id },
|
||||
} = this.app.$route;
|
||||
if (conversation_id) {
|
||||
this.app.$store.dispatch('syncActiveConversationMessages', {
|
||||
conversationId: Number(conversation_id),
|
||||
});
|
||||
}
|
||||
emitter.emit(BUS_EVENTS.WEBSOCKET_DISCONNECT);
|
||||
};
|
||||
|
||||
isAValidEvent = data => {
|
||||
@@ -177,8 +159,8 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
fetchConversationStats = () => {
|
||||
bus.$emit('fetch_conversation_stats');
|
||||
bus.$emit('fetch_overview_reports');
|
||||
emitter.emit('fetch_conversation_stats');
|
||||
emitter.emit('fetch_overview_reports');
|
||||
};
|
||||
|
||||
onContactDelete = data => {
|
||||
@@ -207,7 +189,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
onFirstReplyCreated = () => {
|
||||
bus.$emit('fetch_overview_reports');
|
||||
emitter.emit('fetch_overview_reports');
|
||||
};
|
||||
|
||||
onCacheInvalidate = data => {
|
||||
|
||||
@@ -87,7 +87,8 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||
};
|
||||
|
||||
export const getInboxWarningIconClass = (type, reauthorizationRequired) => {
|
||||
if (type === INBOX_TYPES.FB && reauthorizationRequired) {
|
||||
const allowedInboxTypes = [INBOX_TYPES.FB, INBOX_TYPES.EMAIL];
|
||||
if (allowedInboxTypes.includes(type) && reauthorizationRequired) {
|
||||
return 'warning';
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import NotificationSubscriptions from '../api/notificationSubscription';
|
||||
import auth from '../api/auth';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
@@ -68,7 +69,7 @@ export const registerSubscription = (onSuccess = () => {}) => {
|
||||
onSuccess();
|
||||
})
|
||||
.catch(() => {
|
||||
window.bus.$emit(
|
||||
emitter.emit(
|
||||
'newToastMessage',
|
||||
'This browser does not support desktop notification'
|
||||
);
|
||||
@@ -77,7 +78,7 @@ export const registerSubscription = (onSuccess = () => {}) => {
|
||||
|
||||
export const requestPushPermissions = ({ onSuccess }) => {
|
||||
if (!('Notification' in window)) {
|
||||
window.bus.$emit(
|
||||
emitter.emit(
|
||||
'newToastMessage',
|
||||
'This browser does not support desktop notification'
|
||||
);
|
||||
|
||||
@@ -109,5 +109,14 @@ export const getConversationDashboardRoute = routeName => {
|
||||
}
|
||||
};
|
||||
|
||||
export const isAInboxViewRoute = routeName =>
|
||||
['inbox_view_conversation'].includes(routeName);
|
||||
export const isAInboxViewRoute = (routeName, includeBase = false) => {
|
||||
const baseRoutes = ['inbox_view'];
|
||||
const extendedRoutes = ['inbox_view_conversation'];
|
||||
const routeNames = includeBase
|
||||
? [...baseRoutes, ...extendedRoutes]
|
||||
: extendedRoutes;
|
||||
return routeNames.includes(routeName);
|
||||
};
|
||||
|
||||
export const isNotificationRoute = routeName =>
|
||||
routeName === 'notifications_index';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AnalyticsHelper from './AnalyticsHelper';
|
||||
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
||||
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
|
||||
@@ -8,7 +9,7 @@ export const ANALYTICS_IDENTITY = 'ANALYTICS_IDENTITY';
|
||||
export const ANALYTICS_RESET = 'ANALYTICS_RESET';
|
||||
|
||||
export const initializeAnalyticsEvents = () => {
|
||||
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
|
||||
emitter.on(ANALYTICS_IDENTITY, ({ user }) => {
|
||||
AnalyticsHelper.identify(user);
|
||||
});
|
||||
};
|
||||
@@ -34,12 +35,12 @@ const initializeAudioAlerts = user => {
|
||||
};
|
||||
|
||||
export const initializeChatwootEvents = () => {
|
||||
window.bus.$on(CHATWOOT_RESET, () => {
|
||||
emitter.on(CHATWOOT_RESET, () => {
|
||||
if (window.$chatwoot) {
|
||||
window.$chatwoot.reset();
|
||||
}
|
||||
});
|
||||
window.bus.$on(CHATWOOT_SET_USER, ({ user }) => {
|
||||
emitter.on(CHATWOOT_SET_USER, ({ user }) => {
|
||||
if (window.$chatwoot) {
|
||||
window.$chatwoot.setUser(user.email, {
|
||||
avatar_url: user.avatar_url,
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { differenceInSeconds } from 'date-fns';
|
||||
import {
|
||||
isAConversationRoute,
|
||||
isAInboxViewRoute,
|
||||
isNotificationRoute,
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
import ReconnectService from 'dashboard/helper/ReconnectService';
|
||||
|
||||
jest.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('date-fns', () => ({
|
||||
differenceInSeconds: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('dashboard/helper/routeHelpers', () => ({
|
||||
isAConversationRoute: jest.fn(),
|
||||
isAInboxViewRoute: jest.fn(),
|
||||
isNotificationRoute: jest.fn(),
|
||||
}));
|
||||
|
||||
const storeMock = {
|
||||
dispatch: jest.fn(),
|
||||
getters: {
|
||||
getAppliedConversationFiltersQuery: [],
|
||||
'customViews/getActiveConversationFolder': { query: {} },
|
||||
'notifications/getNotificationFilters': {},
|
||||
},
|
||||
};
|
||||
|
||||
const routerMock = {
|
||||
currentRoute: {
|
||||
name: '',
|
||||
params: { conversation_id: null },
|
||||
},
|
||||
};
|
||||
|
||||
describe('ReconnectService', () => {
|
||||
let reconnectService;
|
||||
|
||||
beforeEach(() => {
|
||||
window.addEventListener = jest.fn();
|
||||
window.removeEventListener = jest.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: { reload: jest.fn() },
|
||||
});
|
||||
reconnectService = new ReconnectService(storeMock, routerMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with store, router, and setup event listeners', () => {
|
||||
expect(reconnectService.store).toBe(storeMock);
|
||||
expect(reconnectService.router).toBe(routerMock);
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'online',
|
||||
reconnectService.handleOnlineEvent
|
||||
);
|
||||
expect(emitter.on).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_RECONNECT,
|
||||
reconnectService.onReconnect
|
||||
);
|
||||
expect(emitter.on).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_DISCONNECT,
|
||||
reconnectService.onDisconnect
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('should remove event listeners', () => {
|
||||
reconnectService.disconnect();
|
||||
expect(window.removeEventListener).toHaveBeenCalledWith(
|
||||
'online',
|
||||
reconnectService.handleOnlineEvent
|
||||
);
|
||||
expect(emitter.off).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_RECONNECT,
|
||||
reconnectService.onReconnect
|
||||
);
|
||||
expect(emitter.off).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_DISCONNECT,
|
||||
reconnectService.onDisconnect
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSecondsSinceDisconnect', () => {
|
||||
it('should return 0 if disconnectTime is null', () => {
|
||||
reconnectService.disconnectTime = null;
|
||||
expect(reconnectService.getSecondsSinceDisconnect()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return the number of seconds since disconnect', () => {
|
||||
reconnectService.disconnectTime = new Date();
|
||||
differenceInSeconds.mockReturnValue(100);
|
||||
expect(reconnectService.getSecondsSinceDisconnect()).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleOnlineEvent', () => {
|
||||
it('should reload the page if disconnected for more than 3 hours', () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
.fn()
|
||||
.mockReturnValue(10801);
|
||||
reconnectService.handleOnlineEvent();
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not reload the page if disconnected for less than 3 hours', () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
.fn()
|
||||
.mockReturnValue(10799);
|
||||
reconnectService.handleOnlineEvent();
|
||||
expect(window.location.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConversations', () => {
|
||||
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
.fn()
|
||||
.mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
page: null,
|
||||
updatedWithin: 100,
|
||||
});
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations');
|
||||
});
|
||||
|
||||
it('should dispatch updateChatListFilters and reset updatedWithin', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
.fn()
|
||||
.mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
updatedWithin: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFilteredOrSavedConversations', () => {
|
||||
it('should dispatch fetchFilteredConversations', async () => {
|
||||
const payload = { test: 'data' };
|
||||
await reconnectService.fetchFilteredOrSavedConversations(payload);
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'fetchFilteredConversations',
|
||||
{ queryData: payload, page: 1 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConversationsOnReconnect', () => {
|
||||
it('should fetch filtered or saved conversations if query exists', async () => {
|
||||
storeMock.getters.getAppliedConversationFiltersQuery = {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const spy = jest.spyOn(
|
||||
reconnectService,
|
||||
'fetchFilteredOrSavedConversations'
|
||||
);
|
||||
|
||||
await reconnectService.fetchConversationsOnReconnect();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
storeMock.getters.getAppliedConversationFiltersQuery
|
||||
);
|
||||
});
|
||||
|
||||
it('should fetch all conversations if no query exists', async () => {
|
||||
storeMock.getters.getAppliedConversationFiltersQuery = [];
|
||||
storeMock.getters['customViews/getActiveConversationFolder'] = {
|
||||
query: null,
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(reconnectService, 'fetchConversations');
|
||||
|
||||
await reconnectService.fetchConversationsOnReconnect();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch filtered or saved conversations if active folder query exists and no applied query', async () => {
|
||||
storeMock.getters.getAppliedConversationFiltersQuery = [];
|
||||
storeMock.getters['customViews/getActiveConversationFolder'] = {
|
||||
query: { test: 'activeFolderQuery' },
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(
|
||||
reconnectService,
|
||||
'fetchFilteredOrSavedConversations'
|
||||
);
|
||||
|
||||
await reconnectService.fetchConversationsOnReconnect();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({ test: 'activeFolderQuery' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConversationMessagesOnReconnect', () => {
|
||||
it('should dispatch syncActiveConversationMessages if conversationId exists', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = 1;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
{ conversationId: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch syncActiveConversationMessages if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = null;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchNotificationsOnReconnect', () => {
|
||||
it('should dispatch notifications/index', async () => {
|
||||
const filter = { test: 'filter' };
|
||||
await reconnectService.fetchNotificationsOnReconnect(filter);
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('notifications/index', {
|
||||
...filter,
|
||||
page: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('revalidateCaches', () => {
|
||||
it('should dispatch revalidate actions for labels, inboxes, and teams', async () => {
|
||||
storeMock.dispatch.mockResolvedValueOnce({
|
||||
label: 'labelKey',
|
||||
inbox: 'inboxKey',
|
||||
team: 'teamKey',
|
||||
});
|
||||
await reconnectService.revalidateCaches();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('accounts/getCacheKeys');
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('labels/revalidate', {
|
||||
newKey: 'labelKey',
|
||||
});
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('inboxes/revalidate', {
|
||||
newKey: 'inboxKey',
|
||||
});
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('teams/revalidate', {
|
||||
newKey: 'teamKey',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRouteSpecificFetch', () => {
|
||||
it('should fetch conversations and messages if current route is a conversation route', async () => {
|
||||
isAConversationRoute.mockReturnValue(true);
|
||||
const spyConversations = jest.spyOn(
|
||||
reconnectService,
|
||||
'fetchConversationsOnReconnect'
|
||||
);
|
||||
const spyMessages = jest.spyOn(
|
||||
reconnectService,
|
||||
'fetchConversationMessagesOnReconnect'
|
||||
);
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spyConversations).toHaveBeenCalled();
|
||||
expect(spyMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch notifications if current route is an inbox view route', async () => {
|
||||
isAInboxViewRoute.mockReturnValue(true);
|
||||
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch notifications if current route is a notification route', async () => {
|
||||
isNotificationRoute.mockReturnValue(true);
|
||||
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConversationLastMessageId', () => {
|
||||
it('should dispatch setConversationLastMessageId if conversationId exists', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = 1;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
{ conversationId: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch setConversationLastMessageId if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = null;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDisconnect', () => {
|
||||
it('should set disconnectTime and call setConversationLastMessageId', () => {
|
||||
reconnectService.setConversationLastMessageId = jest.fn();
|
||||
reconnectService.onDisconnect();
|
||||
expect(reconnectService.disconnectTime).toBeInstanceOf(Date);
|
||||
expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onReconnect', () => {
|
||||
it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => {
|
||||
reconnectService.handleRouteSpecificFetch = jest.fn();
|
||||
reconnectService.revalidateCaches = jest.fn();
|
||||
await reconnectService.onReconnect();
|
||||
expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled();
|
||||
expect(reconnectService.revalidateCaches).toHaveBeenCalled();
|
||||
expect(emitter.emit).toHaveBeenCalledWith(
|
||||
BUS_EVENTS.WEBSOCKET_RECONNECT_COMPLETED
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -186,4 +186,12 @@ describe('isAInboxViewRoute', () => {
|
||||
expect(isAInboxViewRoute('inbox_view_conversation')).toBe(true);
|
||||
expect(isAInboxViewRoute('inbox_conversation')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if base inbox view route name is provided and includeBase is true', () => {
|
||||
expect(isAInboxViewRoute('inbox_view', true)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if base inbox view route name is provided and includeBase is false', () => {
|
||||
expect(isAInboxViewRoute('inbox_view')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,7 +95,9 @@
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"OFFLINE": "Offline"
|
||||
"OFFLINE": "Offline",
|
||||
"RECONNECTING": "Reconnecting...",
|
||||
"RECONNECT_SUCCESS": "Reconnected"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "Refresh"
|
||||
|
||||
@@ -366,8 +366,17 @@
|
||||
"TITLE": "Microsoft Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
},
|
||||
"GOOGLE": {
|
||||
"TITLE": "Google Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"SIGN_IN": "Sign in with Google",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"HELP": "To add your Google account as a channel, you need to authenticate your Google account by clicking on 'Sign in with Google' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
@@ -735,7 +744,8 @@
|
||||
},
|
||||
"EMAIL_PROVIDERS": {
|
||||
"MICROSOFT": "Microsoft",
|
||||
"GOOGLE": "Google",
|
||||
"OTHER_PROVIDERS": "Other Providers"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
"SLA": "SLA",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
"HELP_CENTER": {
|
||||
"TITLE": "Help Center",
|
||||
"ALL_ARTICLES": "All Articles",
|
||||
|
||||
@@ -110,11 +110,11 @@ export default {
|
||||
mounted() {
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
bus.$on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
this.$emitter.on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
bus.$off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
this.$emitter.off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ICON_REOPEN_CONVERSATION,
|
||||
ICON_RESOLVE_CONVERSATION,
|
||||
} from './CommandBarIcons';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import { createSnoozeHandlers } from './commandBarActions';
|
||||
|
||||
@@ -37,7 +38,7 @@ export const RESOLVED_CONVERSATION_BULK_ACTIONS = [
|
||||
title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_REOPEN_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_BULK_ACTION_REOPEN_CONVERSATION),
|
||||
handler: () => emitter.emit(CMD_BULK_ACTION_REOPEN_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,7 +48,7 @@ export const OPEN_CONVERSATION_BULK_ACTIONS = [
|
||||
title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.BULK_ACTIONS',
|
||||
icon: ICON_RESOLVE_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_BULK_ACTION_RESOLVE_CONVERSATION),
|
||||
handler: () => emitter.emit(CMD_BULK_ACTION_RESOLVE_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import {
|
||||
CMD_MUTE_CONVERSATION,
|
||||
@@ -26,7 +27,7 @@ export const OPEN_CONVERSATION_ACTIONS = [
|
||||
title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_RESOLVE_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_RESOLVE_CONVERSATION),
|
||||
handler: () => emitter.emit(CMD_RESOLVE_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -37,7 +38,7 @@ export const createSnoozeHandlers = (busEventName, parentId, section) => {
|
||||
parent: parentId,
|
||||
section: section,
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () => bus.$emit(busEventName, option),
|
||||
handler: () => emitter.emit(busEventName, option),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -61,7 +62,7 @@ export const RESOLVED_CONVERSATION_ACTIONS = [
|
||||
title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_REOPEN_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_REOPEN_CONVERSATION),
|
||||
handler: () => emitter.emit(CMD_REOPEN_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -70,7 +71,7 @@ export const SEND_TRANSCRIPT_ACTION = {
|
||||
title: 'COMMAND_BAR.COMMANDS.SEND_TRANSCRIPT',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_SEND_TRANSCRIPT,
|
||||
handler: () => bus.$emit(CMD_SEND_TRANSCRIPT),
|
||||
handler: () => emitter.emit(CMD_SEND_TRANSCRIPT),
|
||||
};
|
||||
|
||||
export const UNMUTE_ACTION = {
|
||||
@@ -78,7 +79,7 @@ export const UNMUTE_ACTION = {
|
||||
title: 'COMMAND_BAR.COMMANDS.UNMUTE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_UNMUTE_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_UNMUTE_CONVERSATION),
|
||||
handler: () => emitter.emit(CMD_UNMUTE_CONVERSATION),
|
||||
};
|
||||
|
||||
export const MUTE_ACTION = {
|
||||
@@ -86,5 +87,5 @@ export const MUTE_ACTION = {
|
||||
title: 'COMMAND_BAR.COMMANDS.MUTE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_MUTE_CONVERSATION,
|
||||
handler: () => bus.$emit(CMD_MUTE_CONVERSATION),
|
||||
handler: () => emitter.emit(CMD_MUTE_CONVERSATION),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import { CMD_AI_ASSIST } from './commandBarBusEvents';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
@@ -318,7 +319,7 @@ export default {
|
||||
section: this.$t('COMMAND_BAR.SECTIONS.AI_ASSIST'),
|
||||
priority: item,
|
||||
icon: item.icon,
|
||||
handler: () => bus.$emit(CMD_AI_ASSIST, item.key),
|
||||
handler: () => emitter.emit(CMD_AI_ASSIST, item.key),
|
||||
}));
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import { CMD_SNOOZE_NOTIFICATION } from './commandBarBusEvents';
|
||||
import { ICON_SNOOZE_NOTIFICATION } from './CommandBarIcons';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers';
|
||||
|
||||
@@ -21,7 +22,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW),
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_TOMORROW,
|
||||
@@ -30,7 +31,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_TOMORROW),
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_TOMORROW),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_WEEK,
|
||||
@@ -39,7 +40,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK),
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_NEXT_MONTH,
|
||||
@@ -48,7 +49,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH),
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH),
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME,
|
||||
@@ -57,7 +58,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
handler: () =>
|
||||
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME),
|
||||
emitter.emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME),
|
||||
},
|
||||
];
|
||||
export default {
|
||||
|
||||
@@ -19,12 +19,12 @@ const initiatedAt = computed(
|
||||
() => props.conversationAttributes.initiated_at?.timestamp
|
||||
);
|
||||
|
||||
const browserInfo = props.conversationAttributes.browser;
|
||||
const browserInfo = computed(() => props.conversationAttributes.browser);
|
||||
|
||||
const browserName = computed(() => {
|
||||
if (!browserInfo) return '';
|
||||
if (!browserInfo.value) return '';
|
||||
const { browser_name: name = '', browser_version: version = '' } =
|
||||
browserInfo;
|
||||
browserInfo.value;
|
||||
return `${name} ${version}`;
|
||||
});
|
||||
|
||||
@@ -33,9 +33,9 @@ const browserLanguage = computed(() =>
|
||||
);
|
||||
|
||||
const platformName = computed(() => {
|
||||
if (!browserInfo) return '';
|
||||
if (!browserInfo.value) return '';
|
||||
const { platform_name: name = '', platform_version: version = '' } =
|
||||
browserInfo;
|
||||
browserInfo.value;
|
||||
return `${name} ${version}`;
|
||||
});
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ export default {
|
||||
after: messageId,
|
||||
})
|
||||
.then(() => {
|
||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE, { messageId });
|
||||
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, { messageId });
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch('clearSelectedState');
|
||||
|
||||
@@ -271,7 +271,10 @@ export default {
|
||||
},
|
||||
toggleConversationModal() {
|
||||
this.showConversationModal = !this.showConversationModal;
|
||||
bus.$emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, this.showConversationModal);
|
||||
this.$emitter.emit(
|
||||
BUS_EVENTS.NEW_CONVERSATION_MODAL,
|
||||
this.showConversationModal
|
||||
);
|
||||
},
|
||||
toggleDeleteModal() {
|
||||
this.showDeleteModal = !this.showDeleteModal;
|
||||
|
||||
+2
-2
@@ -265,7 +265,7 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
bus.$on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
this.$emitter.on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) this.lastActivePortalSlug = slug;
|
||||
@@ -273,7 +273,7 @@ export default {
|
||||
this.fetchPortalAndItsCategories();
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
this.$emitter.off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
updated() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ const locales = computed(() => {
|
||||
});
|
||||
const allowedLocales = computed(() => {
|
||||
return Object.keys(locales.value).map(key => {
|
||||
return this.locales.value[key].code;
|
||||
return locales.value[key].code;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ async function updatePortalLocales({
|
||||
|
||||
function changeDefaultLocale({ localeCode }) {
|
||||
updatePortalLocales({
|
||||
allowedLocales: allowedLocales.value,
|
||||
newAllowedLocales: allowedLocales.value,
|
||||
defaultLocale: localeCode,
|
||||
messageKey: 'CHANGE_DEFAULT_LOCALE',
|
||||
});
|
||||
@@ -90,7 +90,7 @@ function deletePortalLocale({ localeCode }) {
|
||||
const defaultLocale = currentPortal.value?.meta.default_locale;
|
||||
|
||||
updatePortalLocales({
|
||||
allowedLocales: updatedLocales,
|
||||
newAllowedLocales: updatedLocales,
|
||||
defaultLocale,
|
||||
messageKey: 'DELETE_LOCALE',
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import InboxCard from './components/InboxCard.vue';
|
||||
import InboxListHeader from './components/InboxListHeader.vue';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
@@ -103,6 +104,13 @@ export default {
|
||||
return !this.uiFlags.isFetching && !this.notifications.length;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inboxFilters(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$store.dispatch('notifications/updateNotificationFilters', newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setSavedFilter();
|
||||
this.fetchNotifications();
|
||||
@@ -190,6 +198,10 @@ export default {
|
||||
this.status = status;
|
||||
this.type = type;
|
||||
this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
||||
this.$store.dispatch(
|
||||
'notifications/setNotificationFilters',
|
||||
this.inboxFilters
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -140,7 +140,7 @@ export default {
|
||||
this.$store
|
||||
.dispatch('setActiveChat', { data: selectedConversation })
|
||||
.then(() => {
|
||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
});
|
||||
},
|
||||
findConversation() {
|
||||
|
||||
@@ -92,10 +92,10 @@ export default {
|
||||
...mapGetters({ meta: 'notifications/getMeta' }),
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
|
||||
this.$emitter.on(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
|
||||
this.$emitter.off(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
|
||||
},
|
||||
methods: {
|
||||
openSnoozeNotificationModal() {
|
||||
|
||||
@@ -200,7 +200,7 @@ export default {
|
||||
},
|
||||
|
||||
featureInboundEmailEnabled() {
|
||||
return !!this.features.inbound_emails;
|
||||
return !!this.features?.inbound_emails;
|
||||
},
|
||||
|
||||
featureCustomReplyDomainEnabled() {
|
||||
|
||||
@@ -69,8 +69,10 @@
|
||||
<script>
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -117,9 +119,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
showAlert(message) {
|
||||
bus.$emit('newToastMessage', message);
|
||||
},
|
||||
async addAgent() {
|
||||
try {
|
||||
await this.$store.dispatch('agents/create', {
|
||||
|
||||
@@ -83,6 +83,7 @@ import WootSubmitButton from '../../../../components/buttons/FormSubmitButton.vu
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
import Auth from '../../../../api/auth';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||
|
||||
@@ -91,6 +92,7 @@ export default {
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
@@ -169,9 +171,6 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showAlert(message) {
|
||||
bus.$emit('newToastMessage', message);
|
||||
},
|
||||
async editAgent() {
|
||||
try {
|
||||
await this.$store.dispatch('agents/update', {
|
||||
|
||||
@@ -130,6 +130,7 @@ import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Thumbnail from '../../../../components/widgets/Thumbnail.vue';
|
||||
import AddAgent from './AddAgent.vue';
|
||||
import EditAgent from './EditAgent.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -137,7 +138,7 @@ export default {
|
||||
EditAgent,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
mixins: [globalConfigMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
@@ -230,19 +231,19 @@ export default {
|
||||
async deleteAgent(id) {
|
||||
try {
|
||||
await this.$store.dispatch('agents/delete', id);
|
||||
this.showAlert(this.$t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
this.showAlertMessage(this.$t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
this.showAlertMessage(this.$t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
// Show SnackBar
|
||||
showAlert(message) {
|
||||
showAlertMessage(message) {
|
||||
// Reset loading, current selected agent
|
||||
this.loading[this.currentAgent.id] = false;
|
||||
this.currentAgent = {};
|
||||
// Show message
|
||||
this.agentAPI.message = message;
|
||||
bus.$emit('newToastMessage', message);
|
||||
this.showAlert(message);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -132,12 +132,14 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddCanned from './AddCanned.vue';
|
||||
import EditCanned from './EditCanned.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddCanned,
|
||||
EditCanned,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
@@ -187,13 +189,13 @@ export default {
|
||||
});
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
},
|
||||
showAlert(message) {
|
||||
showAlertMessage(message) {
|
||||
// Reset loading, current selected agent
|
||||
this.loading[this.selectedResponse.id] = false;
|
||||
this.selectedResponse = {};
|
||||
// Show message
|
||||
this.cannedResponseAPI.message = message;
|
||||
bus.$emit('newToastMessage', message);
|
||||
this.showAlert(message);
|
||||
},
|
||||
// Edit Function
|
||||
openAddPopup() {
|
||||
@@ -230,12 +232,14 @@ export default {
|
||||
this.$store
|
||||
.dispatch('deleteCannedResponse', id)
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('CANNED_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
this.showAlertMessage(
|
||||
this.$t('CANNED_MGMT.DELETE.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMessage =
|
||||
error?.message || this.$t('CANNED_MGMT.DELETE.API.ERROR_MESSAGE');
|
||||
this.showAlert(errorMessage);
|
||||
this.showAlertMessage(errorMessage);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -51,11 +51,13 @@ import { mapGetters } from 'vuex';
|
||||
import InboxMembersAPI from '../../../../api/inboxMembers';
|
||||
import router from '../../../index';
|
||||
import PageHeader from '../SettingsSubPageHeader.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageHeader,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
|
||||
validations: {
|
||||
selectedAgents: {
|
||||
@@ -98,7 +100,7 @@ export default {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
bus.$emit('newToastMessage', error.message);
|
||||
this.showAlert(error.message);
|
||||
}
|
||||
this.isCreating = false;
|
||||
},
|
||||
|
||||
@@ -136,12 +136,13 @@ import Settings from './Settings.vue';
|
||||
import adminMixin from '../../../../mixins/isAdmin';
|
||||
import accountMixin from '../../../../mixins/account';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Settings,
|
||||
},
|
||||
mixins: [adminMixin, accountMixin, globalConfigMixin],
|
||||
mixins: [adminMixin, accountMixin, globalConfigMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
@@ -194,15 +195,9 @@ export default {
|
||||
async deleteInbox({ id }) {
|
||||
try {
|
||||
await this.$store.dispatch('inboxes/delete', id);
|
||||
bus.$emit(
|
||||
'newToastMessage',
|
||||
this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
this.showAlert(this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
bus.$emit(
|
||||
'newToastMessage',
|
||||
this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')
|
||||
);
|
||||
this.showAlert(this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</setting-intro-banner>
|
||||
|
||||
<microsoft-reauthorize v-if="microsoftUnauthorized" :inbox="inbox" />
|
||||
<facebook-reauthorize v-if="facebookUnauthorized" :inbox="inbox" />
|
||||
|
||||
<div v-if="selectedTabKey === 'inbox_settings'" class="mx-8">
|
||||
<settings-section
|
||||
@@ -399,7 +400,6 @@
|
||||
@click="updateInbox"
|
||||
/>
|
||||
</settings-section>
|
||||
<facebook-reauthorize v-if="isAFacebookInbox" :inbox-id="inbox.id" />
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTabKey === 'collaborators'" class="mx-8">
|
||||
@@ -541,7 +541,8 @@ export default {
|
||||
this.isALineChannel ||
|
||||
this.isAPIInbox ||
|
||||
(this.isAnEmailChannel && !this.inbox.provider) ||
|
||||
(this.isAnEmailChannel && this.inbox.provider === 'microsoft') ||
|
||||
this.isAMicrosoftInbox ||
|
||||
this.isAGoogleInbox ||
|
||||
this.isAWhatsAppChannel ||
|
||||
this.isAWebWidgetInbox
|
||||
) {
|
||||
@@ -619,7 +620,10 @@ export default {
|
||||
return false;
|
||||
},
|
||||
microsoftUnauthorized() {
|
||||
return this.inbox.microsoft_reauthorization;
|
||||
return this.isAMicrosoftInbox && this.inbox.reauthorization_required;
|
||||
},
|
||||
facebookUnauthorized() {
|
||||
return this.isAFacebookInbox && this.inbox.reauthorization_required;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<microsoft v-else-if="provider === 'microsoft'" />
|
||||
<google v-else-if="provider === 'google'" />
|
||||
<forward-to-option v-else-if="provider === 'other_provider'" />
|
||||
</template>
|
||||
<script>
|
||||
import ForwardToOption from './emailChannels/ForwardToOption.vue';
|
||||
import Microsoft from './emailChannels/Microsoft.vue';
|
||||
import Google from './emailChannels/Google.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
@@ -34,6 +36,7 @@ export default {
|
||||
ChannelSelector,
|
||||
ForwardToOption,
|
||||
Microsoft,
|
||||
Google,
|
||||
PageHeader,
|
||||
},
|
||||
data() {
|
||||
@@ -52,6 +55,12 @@ export default {
|
||||
key: 'microsoft',
|
||||
src: '/assets/images/dashboard/channels/microsoft.png',
|
||||
},
|
||||
{
|
||||
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.GOOGLE'),
|
||||
isEnabled: !!window.chatwootConfig.googleOAuthClientId,
|
||||
key: 'google',
|
||||
src: '/assets/images/dashboard/channels/google.png',
|
||||
},
|
||||
{
|
||||
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.OTHER_PROVIDERS'),
|
||||
isEnabled: true,
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
|
||||
import googleClient from 'dashboard/api/channel/googleClient';
|
||||
import SettingsSubPageHeader from '../../../SettingsSubPageHeader.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isRequestingAuthorization = ref(false);
|
||||
const email = ref('');
|
||||
|
||||
async function requestAuthorization() {
|
||||
try {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await googleClient.generateAuthorization({
|
||||
email: email.value,
|
||||
});
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.ADD.GOOGLE.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
isRequestingAuthorization.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<settings-sub-page-header
|
||||
:header-title="$t('INBOX_MGMT.ADD.GOOGLE.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.GOOGLE.DESCRIPTION')"
|
||||
/>
|
||||
<form class="mt-6" @submit.prevent="requestAuthorization">
|
||||
<woot-input
|
||||
v-model="email"
|
||||
type="email"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.GOOGLE.EMAIL_PLACEHOLDER')"
|
||||
/>
|
||||
<woot-submit-button
|
||||
icon="brand-twitter"
|
||||
:button-text="$t('INBOX_MGMT.ADD.GOOGLE.SIGN_IN')"
|
||||
type="submit"
|
||||
:loading="isRequestingAuthorization"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
+1
-1
@@ -18,8 +18,8 @@
|
||||
/>
|
||||
<woot-submit-button
|
||||
icon="brand-twitter"
|
||||
button-text="Sign in with Microsoft"
|
||||
type="submit"
|
||||
:button-text="$t('INBOX_MGMT.ADD.MICROSOFT.SIGN_IN')"
|
||||
:loading="isRequestingAuthorization"
|
||||
/>
|
||||
</form>
|
||||
|
||||
@@ -1,38 +1,32 @@
|
||||
<template>
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.FACEBOOK_REAUTHORIZE.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.FACEBOOK_REAUTHORIZE.SUBTITLE')"
|
||||
>
|
||||
<a class="fb--login" href="#" @click="tryFBlogin">
|
||||
<img
|
||||
src="~dashboard/assets/images/channels/facebook_login.png"
|
||||
alt="Facebook-logo"
|
||||
/>
|
||||
</a>
|
||||
</settings-section>
|
||||
<inbox-reconnection-required class="mx-8 mt-5" @reauthorize="tryFBlogin" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global FB */
|
||||
import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||
import InboxReconnectionRequired from '../components/InboxReconnectionRequired';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsSection,
|
||||
InboxReconnectionRequired,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
inboxId: {
|
||||
type: Number,
|
||||
inbox: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inboxId() {
|
||||
return this.inbox.id;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initFB();
|
||||
this.loadFBsdk();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initFB() {
|
||||
if (window.fbSDKLoaded === undefined) {
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
this.$store.dispatch('agents/get');
|
||||
this.fetchAllData();
|
||||
|
||||
bus.$on('fetch_overview_reports', () => {
|
||||
this.$emitter.on('fetch_overview_reports', () => {
|
||||
this.fetchAllData();
|
||||
});
|
||||
},
|
||||
|
||||
+14
-12
@@ -64,17 +64,19 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
tableData() {
|
||||
return this.agentMetrics.map(agent => {
|
||||
const agentInformation = this.getAgentInformation(agent.id);
|
||||
return {
|
||||
agent: agentInformation.name,
|
||||
email: agentInformation.email,
|
||||
thumbnail: agentInformation.thumbnail,
|
||||
open: agent.metric.open || 0,
|
||||
unattended: agent.metric.unattended || 0,
|
||||
status: agentInformation.availability_status,
|
||||
};
|
||||
});
|
||||
return this.agentMetrics
|
||||
.filter(agentMetric => this.getAgentInformation(agentMetric.id))
|
||||
.map(agent => {
|
||||
const agentInformation = this.getAgentInformation(agent.id);
|
||||
return {
|
||||
agent: agentInformation.name || agentInformation.available_name,
|
||||
email: agentInformation.email,
|
||||
thumbnail: agentInformation.thumbnail,
|
||||
open: agent.metric.open || 0,
|
||||
unattended: agent.metric.unattended || 0,
|
||||
status: agentInformation.availability_status,
|
||||
};
|
||||
});
|
||||
},
|
||||
columns() {
|
||||
return [
|
||||
@@ -130,7 +132,7 @@ export default {
|
||||
this.$emit('page-change', pageIndex);
|
||||
},
|
||||
getAgentInformation(id) {
|
||||
return this.agents.find(agent => agent.id === Number(id));
|
||||
return this.agents?.find(agent => agent.id === Number(id));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,6 +109,10 @@ export const actions = {
|
||||
// silent error
|
||||
}
|
||||
},
|
||||
|
||||
getCacheKeys: async () => {
|
||||
return AccountAPI.getCacheKeys();
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
|
||||
@@ -37,9 +37,10 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
fetchAllConversations: async ({ commit, dispatch }, params) => {
|
||||
fetchAllConversations: async ({ commit, state, dispatch }) => {
|
||||
commit(types.SET_LIST_LOADING_STATUS);
|
||||
try {
|
||||
const params = state.conversationFilters;
|
||||
const {
|
||||
data: { data },
|
||||
} = await ConversationApi.get(params);
|
||||
@@ -446,6 +447,14 @@ const actions = {
|
||||
commit(types.CLEAR_CONVERSATION_FILTERS);
|
||||
},
|
||||
|
||||
setChatListFilters({ commit }, data) {
|
||||
commit(types.SET_CHAT_LIST_FILTERS, data);
|
||||
},
|
||||
|
||||
updateChatListFilters({ commit }, data) {
|
||||
commit(types.UPDATE_CHAT_LIST_FILTERS, data);
|
||||
},
|
||||
|
||||
assignPriority: async ({ dispatch }, { conversationId, priority }) => {
|
||||
try {
|
||||
await ConversationApi.togglePriority({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { applyPageFilters, sortComparator } from './helpers';
|
||||
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||
|
||||
export const getSelectedChatConversation = ({
|
||||
allConversations,
|
||||
@@ -21,6 +22,7 @@ const getters = {
|
||||
const selectedChat = _getters.getSelectedChat;
|
||||
return selectedChat.attachments || [];
|
||||
},
|
||||
getChatListFilters: ({ conversationFilters }) => conversationFilters,
|
||||
getLastEmailInSelectedChat: (stage, _getters) => {
|
||||
const selectedChat = _getters.getSelectedChat;
|
||||
const { messages = [] } = selectedChat;
|
||||
@@ -56,6 +58,10 @@ const getters = {
|
||||
getAppliedConversationFilters: _state => {
|
||||
return _state.appliedFilters;
|
||||
},
|
||||
getAppliedConversationFiltersQuery: _state => {
|
||||
const hasAppliedFilters = _state.appliedFilters.length !== 0;
|
||||
return hasAppliedFilters ? filterQueryGenerator(_state.appliedFilters) : [];
|
||||
},
|
||||
getUnAssignedChats: _state => activeFilters => {
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const isUnAssigned = !conversation.meta.assignee;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { findPendingMessageIndex } from './helpers';
|
||||
import { MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from '../../../../shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const state = {
|
||||
allConversations: [],
|
||||
@@ -19,6 +20,7 @@ const state = {
|
||||
conversationParticipants: [],
|
||||
conversationLastSeen: null,
|
||||
syncConversationsMessages: {},
|
||||
conversationFilters: {},
|
||||
};
|
||||
|
||||
// mutations
|
||||
@@ -31,6 +33,23 @@ export const mutations = {
|
||||
);
|
||||
if (indexInCurrentList < 0) {
|
||||
newAllConversations.push(conversation);
|
||||
} else if (conversation.id !== _state.selectedChatId) {
|
||||
// If the conversation is already in the list, replace it
|
||||
// Added this to fix the issue of the conversation not being updated
|
||||
// When reconnecting to the websocket. If the selectedChatId is not the same as
|
||||
// the conversation.id in the store, replace the existing conversation with the new one
|
||||
newAllConversations[indexInCurrentList] = conversation;
|
||||
} else {
|
||||
// If the conversation is already in the list and selectedChatId is the same,
|
||||
// replace all data except the messages array, attachments, dataFetched, allMessagesLoaded
|
||||
const existingConversation = newAllConversations[indexInCurrentList];
|
||||
newAllConversations[indexInCurrentList] = {
|
||||
...conversation,
|
||||
allMessagesLoaded: existingConversation.allMessagesLoaded,
|
||||
messages: existingConversation.messages,
|
||||
dataFetched: existingConversation.dataFetched,
|
||||
attachments: existingConversation.attachments,
|
||||
};
|
||||
}
|
||||
});
|
||||
_state.allConversations = newAllConversations;
|
||||
@@ -180,8 +199,8 @@ export const mutations = {
|
||||
const { conversation: { unread_count: unreadCount = 0 } = {} } = message;
|
||||
chat.unread_count = unreadCount;
|
||||
if (selectedChatId === conversationId) {
|
||||
window.bus.$emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
window.bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -203,8 +222,8 @@ export const mutations = {
|
||||
};
|
||||
Vue.set(allConversations, currentConversationIndex, currentConversation);
|
||||
if (_state.selectedChatId === conversation.id) {
|
||||
window.bus.$emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
window.bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
_state.allConversations.push(conversation);
|
||||
@@ -286,6 +305,13 @@ export const mutations = {
|
||||
[types.SET_CONTEXT_MENU_CHAT_ID](_state, chatId) {
|
||||
_state.contextMenuChatId = chatId;
|
||||
},
|
||||
|
||||
[types.SET_CHAT_LIST_FILTERS](_state, data) {
|
||||
_state.conversationFilters = data;
|
||||
},
|
||||
[types.UPDATE_CHAT_LIST_FILTERS](_state, data) {
|
||||
_state.conversationFilters = { ..._state.conversationFilters, ...data };
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const state = {
|
||||
isCreating: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
activeConversationFolder: null,
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
@@ -21,6 +22,9 @@ export const getters = {
|
||||
getCustomViewsByFilterType: _state => filterType => {
|
||||
return _state.records.filter(record => record.filter_type === filterType);
|
||||
},
|
||||
getActiveConversationFolder(_state) {
|
||||
return _state.activeConversationFolder;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
@@ -71,6 +75,9 @@ export const actions = {
|
||||
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
setActiveConversationFolder({ commit }, data) {
|
||||
commit(types.SET_ACTIVE_CONVERSATION_FOLDER, data);
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -85,6 +92,10 @@ export const mutations = {
|
||||
[types.SET_CUSTOM_VIEW]: MutationHelpers.set,
|
||||
[types.UPDATE_CUSTOM_VIEW]: MutationHelpers.update,
|
||||
[types.DELETE_CUSTOM_VIEW]: MutationHelpers.destroy,
|
||||
|
||||
[types.SET_ACTIVE_CONVERSATION_FOLDER](_state, folder) {
|
||||
_state.activeConversationFolder = folder;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -159,4 +159,11 @@ export const actions = {
|
||||
clear({ commit }) {
|
||||
commit(types.CLEAR_NOTIFICATIONS);
|
||||
},
|
||||
|
||||
setNotificationFilters: ({ commit }, filters) => {
|
||||
commit(types.SET_NOTIFICATION_FILTERS, filters);
|
||||
},
|
||||
updateNotificationFilters: ({ commit }, filters) => {
|
||||
commit(types.UPDATE_NOTIFICATION_FILTERS, filters);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,4 +24,7 @@ export const getters = {
|
||||
getMeta: $state => {
|
||||
return $state.meta;
|
||||
},
|
||||
getNotificationFilters($state) {
|
||||
return $state.notificationFilters;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const state = {
|
||||
isUpdatingUnreadCount: false,
|
||||
isAllNotificationsLoaded: false,
|
||||
},
|
||||
notificationFilters: {},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -28,6 +28,16 @@ export const mutations = {
|
||||
},
|
||||
[types.SET_NOTIFICATIONS]: ($state, data) => {
|
||||
data.forEach(notification => {
|
||||
// Find existing notification with same primary_actor_id (primary_actor_id is unique)
|
||||
const existingNotification = Object.values($state.records).find(
|
||||
record => record.primary_actor_id === notification.primary_actor_id
|
||||
);
|
||||
// This is to handle the case where the same notification is received multiple times
|
||||
// On reconnect, if there is existing notification with same primary_actor_id,
|
||||
// it will be deleted and the new one will be added. So it will solve with duplicate notification
|
||||
if (existingNotification) {
|
||||
Vue.delete($state.records, existingNotification.id);
|
||||
}
|
||||
Vue.set($state.records, notification.id, {
|
||||
...($state.records[notification.id] || {}),
|
||||
...notification,
|
||||
@@ -85,4 +95,14 @@ export const mutations = {
|
||||
[types.SNOOZE_NOTIFICATION]: ($state, { id, snoozed_until }) => {
|
||||
Vue.set($state.records[id], 'snoozed_until', snoozed_until);
|
||||
},
|
||||
|
||||
[types.SET_NOTIFICATION_FILTERS]: ($state, filters) => {
|
||||
Vue.set($state, 'notificationFilters', filters);
|
||||
},
|
||||
[types.UPDATE_NOTIFICATION_FILTERS]: ($state, filters) => {
|
||||
Vue.set($state, 'notificationFilters', {
|
||||
...$state.notificationFilters,
|
||||
...filters,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -659,4 +659,32 @@ describe('#addMentions', () => {
|
||||
expect(commit.mock.calls).toEqual([[types.SET_CONTEXT_MENU_CHAT_ID, 1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setChatListFilters', () => {
|
||||
it('set chat list filters', () => {
|
||||
const filters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
actions.setChatListFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CHAT_LIST_FILTERS, filters],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateChatListFilters', () => {
|
||||
it('update chat list filters', () => {
|
||||
actions.updateChatListFilters({ commit }, { updatedWithin: 20 });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.UPDATE_CHAT_LIST_FILTERS, { updatedWithin: 20 }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -279,4 +279,45 @@ describe('#getters', () => {
|
||||
expect(getters.getContextMenuChatId(state)).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getChatListFilters', () => {
|
||||
it('get chat list filters', () => {
|
||||
const conversationFilters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
const state = { conversationFilters: conversationFilters };
|
||||
expect(getters.getChatListFilters(state)).toEqual(conversationFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAppliedConversationFiltersQuery', () => {
|
||||
it('get applied conversation filters query', () => {
|
||||
const filtersList = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'snoozed', name: 'Snoozed' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
const state = { appliedFilters: filtersList };
|
||||
expect(getters.getAppliedConversationFiltersQuery(state)).toEqual({
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
query_operator: undefined,
|
||||
values: ['snoozed'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import types from '../../../mutation-types';
|
||||
import { mutations } from '../../conversations';
|
||||
|
||||
jest.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#EMPTY_ALL_CONVERSATION', () => {
|
||||
it('empty conversations', () => {
|
||||
@@ -130,7 +140,7 @@ describe('#mutations', () => {
|
||||
timestamp: 1602256198,
|
||||
},
|
||||
]);
|
||||
expect(global.bus.$emit).not.toHaveBeenCalled();
|
||||
expect(emitter.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('add message to the conversation and emit scrollToMessage if it does not exist in the store', () => {
|
||||
@@ -158,7 +168,7 @@ describe('#mutations', () => {
|
||||
timestamp: 1602256198,
|
||||
},
|
||||
]);
|
||||
expect(global.bus.$emit).toHaveBeenCalledWith('SCROLL_TO_MESSAGE');
|
||||
expect(emitter.emit).toHaveBeenCalledWith('SCROLL_TO_MESSAGE');
|
||||
});
|
||||
|
||||
it('update message if it exist in the store', () => {
|
||||
@@ -195,7 +205,7 @@ describe('#mutations', () => {
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(global.bus.$emit).not.toHaveBeenCalled();
|
||||
expect(emitter.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -279,6 +289,84 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_CONVERSATION', () => {
|
||||
it('set all conversation', () => {
|
||||
const state = { allConversations: [{ id: 1 }] };
|
||||
const data = [{ id: 1, name: 'test' }];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is the same', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, status: 'open' }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
const data = [{ id: 1, name: 'test', status: 'resolved' }];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is the same then do not update messages, attachments, dataFetched, allMessagesLoaded', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
messages: [{ id: 1, content: 'test' }],
|
||||
attachments: [{ id: 1, name: 'test1.png' }],
|
||||
dataFetched: true,
|
||||
allMessagesLoaded: true,
|
||||
},
|
||||
],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
messages: [{ id: 1, content: 'updated message' }],
|
||||
attachments: [{ id: 1, name: 'test.png' }],
|
||||
dataFetched: true,
|
||||
allMessagesLoaded: true,
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
messages: [{ id: 1, content: 'test' }],
|
||||
attachments: [{ id: 1, name: 'test1.png' }],
|
||||
dataFetched: true,
|
||||
allMessagesLoaded: true,
|
||||
},
|
||||
];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(expected);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is not the same', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, status: 'open' }],
|
||||
selectedChatId: 2,
|
||||
};
|
||||
const data = [{ id: 1, name: 'test', status: 'resolved' }];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
|
||||
it('set all conversation in reconnect if selected chat id and conversation id is not the same then update messages', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 1, content: 'test' }] }],
|
||||
selectedChatId: 2,
|
||||
};
|
||||
const data = [
|
||||
{ id: 1, name: 'test', messages: [{ id: 1, content: 'tested' }] },
|
||||
];
|
||||
mutations[types.SET_ALL_CONVERSATION](state, data);
|
||||
expect(state.allConversations).toEqual(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_ATTACHMENTS', () => {
|
||||
it('set all attachments', () => {
|
||||
const state = {
|
||||
@@ -411,4 +499,54 @@ describe('#mutations', () => {
|
||||
expect(state.contextMenuChatId).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_CHAT_LIST_FILTERS', () => {
|
||||
it('set chat list filters', () => {
|
||||
const conversationFilters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
const state = { conversationFilters: conversationFilters };
|
||||
mutations[types.SET_CHAT_LIST_FILTERS](state, conversationFilters);
|
||||
expect(state.conversationFilters).toEqual(conversationFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CHAT_LIST_FILTERS', () => {
|
||||
it('update chat list filters', () => {
|
||||
const conversationFilters = {
|
||||
inboxId: 1,
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
};
|
||||
const state = { conversationFilters: conversationFilters };
|
||||
mutations[types.UPDATE_CHAT_LIST_FILTERS](state, {
|
||||
inboxId: 2,
|
||||
updatedWithin: 20,
|
||||
assigneeType: 'all',
|
||||
});
|
||||
expect(state.conversationFilters).toEqual({
|
||||
inboxId: 2,
|
||||
assigneeType: 'all',
|
||||
status: 'open',
|
||||
sortBy: 'created_at',
|
||||
page: 1,
|
||||
labels: ['label'],
|
||||
teamId: 1,
|
||||
conversationType: 'mention',
|
||||
updatedWithin: 20,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,4 +91,13 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setActiveConversationFolder', () => {
|
||||
it('set active conversation folder', async () => {
|
||||
await actions.setActiveConversationFolder({ commit }, customViewList[0]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACTIVE_CONVERSATION_FOLDER, customViewList[0]],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,4 +76,11 @@ describe('#getters', () => {
|
||||
isDeleting: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('getActiveConversationFolder', () => {
|
||||
const state = { activeConversationFolder: customViewList[0] };
|
||||
expect(getters.getActiveConversationFolder(state)).toEqual(
|
||||
customViewList[0]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,4 +34,12 @@ describe('#mutations', () => {
|
||||
expect(state.records).toEqual(updateCustomViewList);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ACTIVE_CONVERSATION_FOLDER', () => {
|
||||
it('set active conversation folder', () => {
|
||||
const state = { activeConversationFolder: customViewList[0] };
|
||||
mutations[types.SET_ACTIVE_CONVERSATION_FOLDER](state, customViewList[0]);
|
||||
expect(state.activeConversationFolder).toEqual(customViewList[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -283,4 +283,34 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setNotificationFilters', () => {
|
||||
it('set notification filters', async () => {
|
||||
const filters = {
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'all',
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
await actions.setNotificationFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATION_FILTERS, filters],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateNotificationFilters', () => {
|
||||
it('update notification filters', async () => {
|
||||
const filters = {
|
||||
page: 1,
|
||||
status: 'unread',
|
||||
type: 'all',
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
await actions.updateNotificationFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.UPDATE_NOTIFICATION_FILTERS, filters],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,4 +81,18 @@ describe('#getters', () => {
|
||||
};
|
||||
expect(getters.getMeta(state)).toEqual({ unreadCount: 1 });
|
||||
});
|
||||
|
||||
it('getNotificationFilters', () => {
|
||||
const state = {
|
||||
notificationFilters: {
|
||||
page: 1,
|
||||
status: 'unread',
|
||||
type: 'all',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
};
|
||||
expect(getters.getNotificationFilters(state)).toEqual(
|
||||
state.notificationFilters
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,19 +52,19 @@ describe('#mutations', () => {
|
||||
});
|
||||
|
||||
describe('#SET_NOTIFICATIONS', () => {
|
||||
it('set notifications ', () => {
|
||||
it('set notifications', () => {
|
||||
const state = { records: {} };
|
||||
mutations[types.SET_NOTIFICATIONS](state, [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
{ id: 1, primary_actor_id: 1 },
|
||||
{ id: 2, primary_actor_id: 2 },
|
||||
{ id: 3, primary_actor_id: 3 },
|
||||
{ id: 4, primary_actor_id: 4 },
|
||||
]);
|
||||
expect(state.records).toEqual({
|
||||
1: { id: 1 },
|
||||
2: { id: 2 },
|
||||
3: { id: 3 },
|
||||
4: { id: 4 },
|
||||
1: { id: 1, primary_actor_id: 1 },
|
||||
2: { id: 2, primary_actor_id: 2 },
|
||||
3: { id: 3, primary_actor_id: 3 },
|
||||
4: { id: 4, primary_actor_id: 4 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,9 @@ export default {
|
||||
|
||||
SET_CONTEXT_MENU_CHAT_ID: 'SET_CONTEXT_MENU_CHAT_ID',
|
||||
|
||||
SET_CHAT_LIST_FILTERS: 'SET_CHAT_LIST_FILTERS',
|
||||
UPDATE_CHAT_LIST_FILTERS: 'UPDATE_CHAT_LIST_FILTERS',
|
||||
|
||||
// Inboxes
|
||||
SET_INBOXES_UI_FLAG: 'SET_INBOXES_UI_FLAG',
|
||||
SET_INBOXES: 'SET_INBOXES',
|
||||
@@ -147,6 +150,8 @@ export default {
|
||||
DELETE_READ_NOTIFICATIONS: 'DELETE_READ_NOTIFICATIONS',
|
||||
DELETE_ALL_NOTIFICATIONS: 'DELETE_ALL_NOTIFICATIONS',
|
||||
SNOOZE_NOTIFICATION: 'SNOOZE_NOTIFICATION',
|
||||
SET_NOTIFICATION_FILTERS: 'SET_NOTIFICATION_FILTERS',
|
||||
UPDATE_NOTIFICATION_FILTERS: 'UPDATE_NOTIFICATION_FILTERS',
|
||||
|
||||
// Contact Conversation
|
||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||
@@ -239,6 +244,7 @@ export default {
|
||||
ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW',
|
||||
UPDATE_CUSTOM_VIEW: 'UPDATE_CUSTOM_VIEW',
|
||||
DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW',
|
||||
SET_ACTIVE_CONVERSATION_FOLDER: 'SET_ACTIVE_CONVERSATION_FOLDER',
|
||||
|
||||
// Bulk Actions
|
||||
SET_BULK_ACTIONS_FLAG: 'SET_BULK_ACTIONS_FLAG',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../../helper/scriptHelpers';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
Cookies.defaults = { sameSite: 'Lax' };
|
||||
|
||||
@@ -18,8 +19,8 @@ export const setLoadingStatus = (state, status) => {
|
||||
};
|
||||
|
||||
export const setUser = user => {
|
||||
window.bus.$emit(CHATWOOT_SET_USER, { user });
|
||||
window.bus.$emit(ANALYTICS_IDENTITY, { user });
|
||||
emitter.emit(CHATWOOT_SET_USER, { user });
|
||||
emitter.emit(ANALYTICS_IDENTITY, { user });
|
||||
};
|
||||
|
||||
export const getHeaderExpiry = response =>
|
||||
@@ -70,8 +71,8 @@ export const deleteIndexedDBOnLogout = async () => {
|
||||
};
|
||||
|
||||
export const clearCookiesOnLogout = () => {
|
||||
window.bus.$emit(CHATWOOT_RESET);
|
||||
window.bus.$emit(ANALYTICS_RESET);
|
||||
emitter.emit(CHATWOOT_RESET);
|
||||
emitter.emit(ANALYTICS_RESET);
|
||||
clearBrowserSessionCookies();
|
||||
clearLocalStorageOnLogout();
|
||||
const globalConfig = window.globalConfig || {};
|
||||
|
||||
@@ -15,6 +15,8 @@ import WootUiKit from '../dashboard/components';
|
||||
import App from '../dashboard/App';
|
||||
import i18n from '../dashboard/i18n';
|
||||
import createAxios from '../dashboard/helper/APIHelper';
|
||||
import { emitter } from '../shared/helpers/mitt';
|
||||
|
||||
import commonHelpers, { isJSONValid } from '../dashboard/helper/commons';
|
||||
import router, { initalizeRouter } from '../dashboard/routes';
|
||||
import store from '../dashboard/store';
|
||||
@@ -93,7 +95,8 @@ commonHelpers();
|
||||
|
||||
window.WootConstants = constants;
|
||||
window.axios = createAxios(axios);
|
||||
window.bus = new Vue();
|
||||
Vue.prototype.$emitter = emitter;
|
||||
|
||||
initializeChatwootEvents();
|
||||
initializeAnalyticsEvents();
|
||||
initalizeRouter();
|
||||
|
||||
@@ -4,6 +4,7 @@ import VueI18n from 'vue-i18n';
|
||||
import App from '../survey/App.vue';
|
||||
import i18n from '../survey/i18n';
|
||||
import store from '../survey/store';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(Vuelidate);
|
||||
@@ -14,7 +15,7 @@ const i18nConfig = new VueI18n({
|
||||
});
|
||||
|
||||
// Event Bus
|
||||
window.bus = new Vue();
|
||||
Vue.prototype.$emitter = emitter;
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from 'shared/helpers/Validators';
|
||||
import router from '../widget/router';
|
||||
import { directive as onClickaway } from 'vue-clickaway';
|
||||
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
|
||||
const PhoneInput = () => import('../widget/components/Form/PhoneInput');
|
||||
|
||||
@@ -46,7 +46,7 @@ Vue.use(VueFormulate, {
|
||||
},
|
||||
});
|
||||
// Event Bus
|
||||
window.bus = new Vue();
|
||||
Vue.prototype.$emitter = emitter;
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
"video-add-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-1.063c.154-.478.255-.98.294-1.5h.769a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6h-8.5A1.75 1.75 0 0 0 3.5 7.75v3.982A6.517 6.517 0 0 0 2 12.81V7.75A3.25 3.25 0 0 1 5.25 4.5h8.5Zm6.75 3.073L17 9.674v4.651l3.5 2.1V7.573ZM12 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0ZM7 18l.001 2.503a.5.5 0 1 1-1 0V18H3.496a.5.5 0 0 1 0-1H6v-2.5a.5.5 0 1 1 1 0V17h2.497a.5.5 0 0 1 0 1H7Z",
|
||||
"warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z",
|
||||
"wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z",
|
||||
"wifi-outline": "M17.745 10.75a8.292 8.292 0 0 1 1.492 2.07a.75.75 0 1 1-1.336.683a6.797 6.797 0 0 0-1.217-1.692A6.562 6.562 0 0 0 6.19 13.484a.75.75 0 1 1-1.338-.677a8.062 8.062 0 0 1 12.893-2.057Zm-2.102 3.07c.448.447.816.997 1.072 1.582a.75.75 0 1 1-1.374.602a3.719 3.719 0 0 0-.759-1.124a3.592 3.592 0 0 0-5.08 0c-.31.31-.562.689-.747 1.11a.75.75 0 1 1-1.374-.6a5.11 5.11 0 0 1 1.061-1.57a5.092 5.092 0 0 1 7.201 0Zm4.805-5.541c.51.509.99 1.09 1.408 1.697a.75.75 0 1 1-1.234.852a10.822 10.822 0 0 0-1.235-1.489c-4.08-4.08-10.695-4.08-14.775 0c-.422.422-.84.934-1.222 1.484a.75.75 0 0 1-1.232-.855c.43-.62.904-1.2 1.394-1.69c4.665-4.665 12.23-4.665 16.896 0Zm-7.387 8.16a1.5 1.5 0 1 1-2.122 2.122a1.5 1.5 0 0 1 2.122-2.122Z",
|
||||
"whatsapp-outline": "M19.05 4.91A9.816 9.816 0 0 0 12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01zm-7.01 15.24c-1.48 0-2.93-.4-4.2-1.15l-.3-.18l-3.12.82l.83-3.04l-.2-.31a8.264 8.264 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.24-8.24c2.2 0 4.27.86 5.82 2.42a8.183 8.183 0 0 1 2.41 5.83c.02 4.54-3.68 8.23-8.22 8.23zm4.52-6.16c-.25-.12-1.47-.72-1.69-.81c-.23-.08-.39-.12-.56.12c-.17.25-.64.81-.78.97c-.14.17-.29.19-.54.06c-.25-.12-1.05-.39-1.99-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.14-.25-.02-.38.11-.51c.11-.11.25-.29.37-.43s.17-.25.25-.41c.08-.17.04-.31-.02-.43s-.56-1.34-.76-1.84c-.2-.48-.41-.42-.56-.43h-.48c-.17 0-.43.06-.66.31c-.22.25-.86.85-.86 2.07c0 1.22.89 2.4 1.01 2.56c.12.17 1.75 2.67 4.23 3.74c.59.26 1.05.41 1.41.52c.59.19 1.13.16 1.56.1c.48-.07 1.47-.6 1.67-1.18c.21-.58.21-1.07.14-1.18s-.22-.16-.47-.28z",
|
||||
"subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z",
|
||||
"table-switch-outline": [
|
||||
|
||||
@@ -8,6 +8,8 @@ export const BUS_EVENTS = {
|
||||
TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU',
|
||||
ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL',
|
||||
WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT',
|
||||
WEBSOCKET_RECONNECT: 'WEBSOCKET_RECONNECT',
|
||||
WEBSOCKET_RECONNECT_COMPLETED: 'WEBSOCKET_RECONNECT_COMPLETED',
|
||||
TOGGLE_REPLY_TO_MESSAGE: 'TOGGLE_REPLY_TO_MESSAGE',
|
||||
SHOW_TOAST: 'newToastMessage',
|
||||
NEW_CONVERSATION_MODAL: 'newConversationModal',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createConsumer } from '@rails/actioncable';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
const PRESENCE_INTERVAL = 20000;
|
||||
const RECONNECT_INTERVAL = 1000;
|
||||
@@ -27,8 +26,6 @@ class BaseActionCableConnector {
|
||||
BaseActionCableConnector.isDisconnected = true;
|
||||
this.onDisconnected();
|
||||
this.initReconnectTimer();
|
||||
// TODO: Remove this after completing the conversation list refetching
|
||||
window.bus.$emit(BUS_EVENTS.WEBSOCKET_DISCONNECT);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user