Merge branch 'chore/upgrade-to-postcss-8' of https://github.com/chatwoot/chatwoot into feat/widget-multi-getters
This commit is contained in:
commit
7bdfb5b075
151 changed files with 2383 additions and 241 deletions
3
.bundler-audit.yml
Normal file
3
.bundler-audit.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
ignore:
|
||||
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
|
8
Gemfile
8
Gemfile
|
@ -56,8 +56,7 @@ gem 'activerecord-import'
|
|||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'rack-timeout'
|
||||
gem 'webpacker', '~> 5.x'
|
||||
gem 'webpacker', '~> 5.4.0'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
||||
|
@ -122,6 +121,11 @@ gem 'hairtrigger'
|
|||
|
||||
gem 'procore-sift'
|
||||
|
||||
group :production, :staging do
|
||||
# we dont want request timing out in development while using byebug
|
||||
gem 'rack-timeout'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -247,6 +247,7 @@ GEM
|
|||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-protobuf (3.17.3)
|
||||
google-protobuf (3.17.3-universal-darwin)
|
||||
google-protobuf (3.17.3-x86_64-linux)
|
||||
googleapis-common-protos (1.3.11)
|
||||
|
@ -264,6 +265,9 @@ GEM
|
|||
signet (~> 0.14)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.38.0)
|
||||
google-protobuf (~> 3.15)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.38.0-universal-darwin)
|
||||
google-protobuf (~> 3.15)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
|
@ -346,6 +350,7 @@ GEM
|
|||
mime-types-data (3.2021.0704)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.1)
|
||||
mini_portile2 (2.5.3)
|
||||
minitest (5.14.4)
|
||||
mock_redis (0.28.0)
|
||||
ruby2_keywords
|
||||
|
@ -360,6 +365,9 @@ GEM
|
|||
netrc (0.11.0)
|
||||
newrelic_rpm (7.2.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.11.7)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
|
@ -617,6 +625,7 @@ GEM
|
|||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
ruby
|
||||
x86_64-darwin-18
|
||||
x86_64-darwin-20
|
||||
x86_64-darwin-21
|
||||
|
@ -714,7 +723,7 @@ DEPENDENCIES
|
|||
valid_email2
|
||||
web-console
|
||||
webmock
|
||||
webpacker (~> 5.x)
|
||||
webpacker (~> 5.4.0)
|
||||
webpush
|
||||
wisper (= 2.0.0)
|
||||
|
||||
|
|
|
@ -148,6 +148,14 @@ class Messages::Facebook::MessageBuilder
|
|||
}
|
||||
end
|
||||
|
||||
def process_contact_params_result(result)
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
|
@ -155,14 +163,15 @@ class Messages::Facebook::MessageBuilder
|
|||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue Koala::Facebook::ClientError => e
|
||||
result = {}
|
||||
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||
Sentry.capture_exception(e) unless @outgoing_echo
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
}
|
||||
process_contact_params_result(result)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,19 +41,25 @@ class V2::ReportBuilder
|
|||
user
|
||||
when :label
|
||||
label
|
||||
when :team
|
||||
team
|
||||
end
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= account.users.where(id: params[:id]).first
|
||||
@user ||= account.users.find(params[:id])
|
||||
end
|
||||
|
||||
def label
|
||||
@label ||= account.labels.where(id: params[:id]).first
|
||||
@label ||= account.labels.find(params[:id])
|
||||
end
|
||||
|
||||
def team
|
||||
@team ||= account.teams.find(params[:id])
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||
|
||||
def index
|
||||
|
@ -30,10 +30,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def import
|
||||
render json: { error: I18n.t('errors.contacts.import.failed') }, status: :unprocessable_entity and return if params[:import_file].blank?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
import = Current.account.data_imports.create!(data_type: 'contacts')
|
||||
import.import_file.attach(params[:import_file])
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
@ -70,6 +73,18 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
if ::OnlineStatusTracker.get_presence(
|
||||
@contact.account.id, 'Contact', @contact.id
|
||||
)
|
||||
return render_error({ message: I18n.t('contacts.online.delete', contact_name: @contact.name.capitalize) },
|
||||
:unprocessable_entity)
|
||||
end
|
||||
|
||||
@contact.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: Move this to a finder class
|
||||
|
@ -134,4 +149,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
def fetch_contact
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
|
||||
def render_error(error, error_status)
|
||||
render json: error, status: error_status
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,6 +69,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = DateTime.now.utc
|
||||
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
|
@ -112,6 +118,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
|
@ -122,6 +129,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes,
|
||||
custom_attributes: custom_attributes,
|
||||
snoozed_until: params[:snoozed_until]
|
||||
}.merge(status)
|
||||
end
|
||||
|
@ -129,4 +137,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
|
||||
def assignee?
|
||||
@conversation.assignee_id? && current_user == @conversation.assignee
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,6 +100,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
return unless @inbox.web_widget?
|
||||
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
||||
|
||||
@inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]
|
||||
|
|
|
@ -29,6 +29,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
def teams
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
|
|
|
@ -3,10 +3,9 @@ class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
|
|||
|
||||
def index
|
||||
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
|
||||
@accounts_count = number_with_delimiter(Account.all.length)
|
||||
@users_count = number_with_delimiter(User.all.length)
|
||||
@inboxes_count = number_with_delimiter(Inbox.all.length)
|
||||
@conversations_count = number_with_delimiter(Conversation.all.length)
|
||||
@messages_count = number_with_delimiter(Message.all.length)
|
||||
@accounts_count = number_with_delimiter(Account.count)
|
||||
@users_count = number_with_delimiter(User.count)
|
||||
@inboxes_count = number_with_delimiter(Inbox.count)
|
||||
@conversations_count = number_with_delimiter(Conversation.count)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,6 +52,14 @@ class ContactAPI extends ApiClient {
|
|||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
importContacts(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('import_file', file);
|
||||
return axios.post(`${this.url}/import`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
||||
|
|
|
@ -59,6 +59,18 @@ describe('#ContactsAPI', () => {
|
|||
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
it('#importContacts', () => {
|
||||
const file = 'file';
|
||||
contactAPI.importContacts(file);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/import',
|
||||
expect.any(FormData),
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
BIN
app/javascript/dashboard/assets/images/twitter-chat-badge.png
Normal file
BIN
app/javascript/dashboard/assets/images/twitter-chat-badge.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
|
@ -0,0 +1,3 @@
|
|||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
|
@ -71,7 +71,8 @@
|
|||
@include padding($space-large);
|
||||
}
|
||||
|
||||
form {
|
||||
form,
|
||||
.modal-content {
|
||||
@include padding($space-large);
|
||||
align-self: center;
|
||||
|
||||
|
|
|
@ -194,7 +194,7 @@ export default {
|
|||
});
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
getKeyboardListenerParams() {
|
||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
);
|
||||
|
@ -205,7 +205,19 @@ export default {
|
|||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
return {
|
||||
allConversations,
|
||||
activeConversation,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
};
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndJKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
|
@ -214,6 +226,11 @@ export default {
|
|||
}
|
||||
}
|
||||
if (hasPressedAltAndKKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
|
|
|
@ -176,7 +176,9 @@ export default {
|
|||
'.conversations-list .conversation'
|
||||
);
|
||||
if (hasPressedAltAndMKey(e)) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
if (this.$refs.arrowDownButton) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
}
|
||||
}
|
||||
if (hasPressedAltAndEKey(e)) {
|
||||
const activeConversation = document.querySelector(
|
||||
|
|
|
@ -95,10 +95,15 @@ import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
|||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedCommandAndForwardSlash,
|
||||
isEscape,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import router from '../../routes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -276,6 +281,27 @@ export default {
|
|||
if (isEscape(e)) {
|
||||
this.closeKeyShortcutModal();
|
||||
}
|
||||
|
||||
if (hasPressedAltAndCKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('home')) {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
} else if (hasPressedAltAndVKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('contacts_dashboard')) {
|
||||
router.push({ name: 'contacts_dashboard' });
|
||||
}
|
||||
} else if (hasPressedAltAndRKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('settings_account_reports')) {
|
||||
router.push({ name: 'settings_account_reports' });
|
||||
}
|
||||
} else if (hasPressedAltAndSKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('agent_list')) {
|
||||
router.push({ name: 'agent_list' });
|
||||
}
|
||||
}
|
||||
},
|
||||
isCurrentRouteSameAsNavigation(routeName) {
|
||||
return router.currentRoute && router.currentRoute.name === routeName;
|
||||
},
|
||||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<a href="#" :class="computedChildClass(child)">
|
||||
<div class="wrap">
|
||||
<i
|
||||
v-if="computedInboxClass(child)"
|
||||
v-if="menuItem.key === 'inbox'"
|
||||
class="inbox-icon"
|
||||
:class="computedInboxClass(child)"
|
||||
/>
|
||||
|
@ -59,17 +59,10 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
|
||||
import router from '../../routes';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
export default {
|
||||
mixins: [adminMixin, eventListenerMixins],
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
|
@ -124,20 +117,6 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndCKey(e)) {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
if (hasPressedAltAndVKey(e)) {
|
||||
router.push({ name: 'contacts_dashboard' });
|
||||
}
|
||||
if (hasPressedAltAndRKey(e)) {
|
||||
router.push({ name: 'settings_account_reports' });
|
||||
}
|
||||
if (hasPressedAltAndSKey(e)) {
|
||||
router.push({ name: 'settings_home' });
|
||||
}
|
||||
},
|
||||
showItem(item) {
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
},
|
||||
|
|
|
@ -22,19 +22,33 @@
|
|||
src="~dashboard/assets/images/fb-badge.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::TwitterProfile'"
|
||||
v-if="badge === 'twitter-tweet'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/twitter-badge.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::TwilioSms'"
|
||||
v-if="badge === 'twitter-chat'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/twitter-chat-badge.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'whatsapp'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/channels/whatsapp.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'sms'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/channels/sms.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::Line'"
|
||||
id="badge"
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<Thumbnail
|
||||
v-if="!hideThumbnail"
|
||||
:src="currentContact.thumbnail"
|
||||
:badge="chatMetadata.channel"
|
||||
:badge="inboxBadge"
|
||||
class="columns"
|
||||
:username="currentContact.name"
|
||||
:status="currentContact.availability_status"
|
||||
|
@ -68,13 +68,14 @@ import conversationMixin from '../../../mixins/conversations';
|
|||
import timeMixin from '../../../mixins/time';
|
||||
import router from '../../../routes';
|
||||
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
mixins: [timeMixin, conversationMixin, messageFormatterMixin],
|
||||
mixins: [inboxMixin, timeMixin, conversationMixin, messageFormatterMixin],
|
||||
props: {
|
||||
activeLabel: {
|
||||
type: String,
|
||||
|
@ -167,14 +168,14 @@ export default {
|
|||
return this.getPlainText(subject || this.lastMessageInChat.content);
|
||||
},
|
||||
|
||||
chatInbox() {
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
return stateInbox;
|
||||
},
|
||||
|
||||
computedInboxClass() {
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.chatInbox;
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
|
@ -187,11 +188,10 @@ export default {
|
|||
);
|
||||
},
|
||||
inboxName() {
|
||||
const stateInbox = this.chatInbox;
|
||||
const stateInbox = this.inbox;
|
||||
return stateInbox.name || '';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cardClick(chat) {
|
||||
const { activeInbox } = this;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<Thumbnail
|
||||
:src="currentContact.thumbnail"
|
||||
size="40px"
|
||||
:badge="chatMetadata.channel"
|
||||
:badge="inboxBadge"
|
||||
:username="currentContact.name"
|
||||
:status="currentContact.availability_status"
|
||||
/>
|
||||
|
@ -42,6 +42,7 @@ import MoreActions from './MoreActions';
|
|||
import Thumbnail from '../Thumbnail';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
|
@ -49,7 +50,7 @@ export default {
|
|||
MoreActions,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [agentMixin, eventListenerMixins],
|
||||
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
|
@ -78,6 +79,12 @@ export default {
|
|||
return this.chat.meta;
|
||||
},
|
||||
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
return stateInbox;
|
||||
},
|
||||
|
||||
currentContact() {
|
||||
return this.$store.getters['contacts/getContact'](
|
||||
this.chat.meta.sender.id
|
||||
|
|
|
@ -76,7 +76,6 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
|
||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||
import CannedResponse from './CannedResponse';
|
||||
|
@ -108,13 +107,7 @@ export default {
|
|||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [
|
||||
clickaway,
|
||||
inboxMixin,
|
||||
uiSettingsMixin,
|
||||
alertMixin,
|
||||
eventListenerMixins,
|
||||
],
|
||||
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
selectedTweet: {
|
||||
type: [Object, String],
|
||||
|
@ -304,6 +297,15 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Donot use the keyboard listener mixin here as the events here are supposed to be
|
||||
// working even if input/textarea is focussed.
|
||||
document.addEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
methods: {
|
||||
toggleUserMention(currentMentionState) {
|
||||
this.hasUserMention = currentMentionState;
|
||||
|
@ -345,7 +347,10 @@ export default {
|
|||
await this.$store.dispatch('sendMessage', messagePayload);
|
||||
this.$emit('scrollToMessage');
|
||||
} catch (error) {
|
||||
// Error
|
||||
const errorMessage =
|
||||
error?.response?.data?.error ||
|
||||
this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||
this.showAlert(errorMessage);
|
||||
}
|
||||
this.hideEmojiPicker();
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
'conversation.typing_off': this.onTypingOff,
|
||||
'conversation.contact_changed': this.onConversationContactChange,
|
||||
'presence.update': this.onPresenceUpdate,
|
||||
'contact.deleted': this.onContactDelete,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -115,6 +116,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
fetchConversationStats = () => {
|
||||
bus.$emit('fetch_conversation_stats');
|
||||
};
|
||||
|
||||
onContactDelete = data => {
|
||||
this.app.$store.dispatch(
|
||||
'contacts/deleteContactThroughConversations',
|
||||
data.id
|
||||
);
|
||||
this.fetchConversationStats();
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -22,7 +22,10 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
|||
case INBOX_TYPES.EMAIL:
|
||||
return 'ion-ios-email';
|
||||
|
||||
case INBOX_TYPES.TELEGRAM:
|
||||
return 'ion-ios-navigate';
|
||||
|
||||
default:
|
||||
return '';
|
||||
return 'ion-ios-chatbubble';
|
||||
}
|
||||
};
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Vyberte časové pásmo",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "Vi er ikke tilgængelige i øjeblikket. Skriv en besked og vi svarer, når vi er tilbage.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -54,6 +54,35 @@
|
|||
"TITLE": "Create new contact",
|
||||
"DESC": "Add basic information details about the contact."
|
||||
},
|
||||
"IMPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Import",
|
||||
"TITLE": "Import Contacts",
|
||||
"DESC": "Import contacts through a CSV file.",
|
||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
||||
"FORM": {
|
||||
"LABEL": "CSV File",
|
||||
"SUBMIT": "Import",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"BUTTON_LABEL": "Delete Contact",
|
||||
"TITLE": "Delete contact",
|
||||
"DESC": "Delete contact details",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"PLACE_HOLDER": "Please type {contactName} to confirm",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
}
|
||||
},
|
||||
"CONTACT_FORM": {
|
||||
"FORM": {
|
||||
"SUBMIT": "Submit",
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
"CHANGE_AGENT": "Conversation Assignee changed",
|
||||
"CHANGE_TEAM": "Conversation team changed",
|
||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Sent by:",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Select Agent",
|
||||
|
|
|
@ -56,6 +56,11 @@
|
|||
"CHANNEL_AVATAR": {
|
||||
"LABEL": "Channel Avatar"
|
||||
},
|
||||
"CHANNEL_WEBHOOK_URL": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Enter your Webhook URL",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"CHANNEL_DOMAIN": {
|
||||
"LABEL": "Website Domain",
|
||||
"PLACEHOLDER": "Enter your website domain (eg: acme.com)"
|
||||
|
@ -127,11 +132,11 @@
|
|||
"ERROR_MESSAGE": "We were not able to authenticate Twilio credentials, please try again"
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"SMS": {
|
||||
"TITLE": "SMS Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
},
|
||||
|
@ -195,6 +200,10 @@
|
|||
"SUBMIT_BUTTON": "Create LINE Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the LINE channel"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
|
||||
}
|
||||
},
|
||||
"TELEGRAM_CHANNEL": {
|
||||
|
@ -350,7 +359,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -13,18 +13,18 @@
|
|||
"STATUS_TABS": [
|
||||
{
|
||||
"NAME": "Apri",
|
||||
"KEY": "contaaperture"
|
||||
"KEY": "openCount"
|
||||
},
|
||||
{
|
||||
"NAME": "Risolti",
|
||||
"KEY": "Conteggio"
|
||||
"KEY": "allConvCount"
|
||||
}
|
||||
],
|
||||
"ASSIGNEE_TYPE_TABS": [
|
||||
{
|
||||
"NAME": "Miniera",
|
||||
"KEY": "Io",
|
||||
"COUNT_KEY": "contaMinore"
|
||||
"COUNT_KEY": "mineCount"
|
||||
},
|
||||
{
|
||||
"NAME": "Non assegnato",
|
||||
|
@ -40,11 +40,11 @@
|
|||
"CHAT_STATUS_ITEMS": [
|
||||
{
|
||||
"TEXT": "Apri",
|
||||
"VALUE": "Aperto"
|
||||
"VALUE": "open"
|
||||
},
|
||||
{
|
||||
"TEXT": "Risolti",
|
||||
"VALUE": "risolto"
|
||||
"VALUE": "resolved"
|
||||
},
|
||||
{
|
||||
"TEXT": "Pending",
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<span class="close-button" @click="onClose">
|
||||
<i class="ion-android-close close-icon" />
|
||||
</span>
|
||||
<contact-info show-new-message :contact="contact" />
|
||||
<contact-info show-new-message :contact="contact" @panel-close="onClose" />
|
||||
<accordion-item
|
||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||
|
|
|
@ -24,12 +24,12 @@ export default {
|
|||
|
||||
computed: {
|
||||
savedLabels() {
|
||||
const result = this.$store.getters['contactLabels/getContactLabels'](
|
||||
this.contactId
|
||||
const availableContactLabels = this.$store.getters[
|
||||
'contactLabels/getContactLabels'
|
||||
](this.contactId);
|
||||
return this.allLabels.filter(({ title }) =>
|
||||
availableContactLabels.includes(title)
|
||||
);
|
||||
return result.map(value => {
|
||||
return this.allLabels.find(label => label.title === value);
|
||||
});
|
||||
},
|
||||
|
||||
...mapGetters({
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
this-selected-contact-id=""
|
||||
:on-input-search="onInputSearch"
|
||||
:on-toggle-create="onToggleCreate"
|
||||
:on-toggle-import="onToggleImport"
|
||||
:header-title="label"
|
||||
/>
|
||||
<contacts-table
|
||||
|
@ -30,6 +31,9 @@
|
|||
:on-close="closeContactInfoPanel"
|
||||
/>
|
||||
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
|
||||
<woot-modal :show.sync="showImportModal" :on-close="onToggleImport">
|
||||
<import-contacts v-if="showImportModal" :on-close="onToggleImport" />
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -41,6 +45,7 @@ import ContactsTable from './ContactsTable';
|
|||
import ContactInfoPanel from './ContactInfoPanel';
|
||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||
import ImportContacts from './ImportContacts.vue';
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
|
||||
|
@ -51,6 +56,7 @@ export default {
|
|||
TableFooter,
|
||||
ContactInfoPanel,
|
||||
CreateContact,
|
||||
ImportContacts,
|
||||
},
|
||||
props: {
|
||||
label: { type: String, default: '' },
|
||||
|
@ -59,6 +65,7 @@ export default {
|
|||
return {
|
||||
searchQuery: '',
|
||||
showCreateModal: false,
|
||||
showImportModal: false,
|
||||
selectedContactId: '',
|
||||
sortConfig: { name: 'asc' },
|
||||
};
|
||||
|
@ -168,6 +175,9 @@ export default {
|
|||
onToggleCreate() {
|
||||
this.showCreateModal = !this.showCreateModal;
|
||||
},
|
||||
onToggleImport() {
|
||||
this.showImportModal = !this.showImportModal;
|
||||
},
|
||||
onSortChange(params) {
|
||||
this.sortConfig = params;
|
||||
this.fetchContacts(this.meta.currentPage);
|
||||
|
|
|
@ -29,11 +29,20 @@
|
|||
<woot-button
|
||||
color-scheme="success"
|
||||
icon="ion-android-add-circle"
|
||||
@click="onToggleCreate"
|
||||
class="margin-right-small"
|
||||
data-testid="create-new-contact"
|
||||
@click="onToggleCreate"
|
||||
>
|
||||
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
color-scheme="info"
|
||||
icon="ion-android-upload"
|
||||
@click="onToggleImport"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -41,7 +50,6 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
|
@ -63,10 +71,15 @@ export default {
|
|||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onToggleImport: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreateModal: false,
|
||||
showImportModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -78,6 +91,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/_utility-helpers.scss';
|
||||
.page-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
|
||||
<p>
|
||||
{{ $t('IMPORT_CONTACTS.DESC') }}
|
||||
<a :href="csvUrl" download="import-contacts-sample">{{
|
||||
$t('IMPORT_CONTACTS.DOWNLOAD_LABEL')
|
||||
}}</a>
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="row modal-content">
|
||||
<div class="medium-12 columns">
|
||||
<label>
|
||||
<span>{{ $t('IMPORT_CONTACTS.FORM.LABEL') }}</span>
|
||||
<input
|
||||
id="file"
|
||||
ref="file"
|
||||
type="file"
|
||||
accept="text/csv"
|
||||
@change="handleFileUpload"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-button
|
||||
:disabled="uiFlags.isCreating || !file"
|
||||
:loading="uiFlags.isCreating"
|
||||
@click="uploadFile"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('IMPORT_CONTACTS.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '../../../../components/Modal';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
file: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
}),
|
||||
csvUrl() {
|
||||
return '/downloads/import-contacts-sample.csv';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async uploadFile() {
|
||||
try {
|
||||
if (!this.file) return;
|
||||
await this.$store.dispatch('contacts/import', this.file);
|
||||
this.onClose();
|
||||
this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
handleFileUpload() {
|
||||
this.file = this.$refs.file.files[0];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -48,30 +48,59 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="!showNewMessage"
|
||||
class="edit-contact"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="toggleEditModal"
|
||||
>
|
||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<div v-else class="contact-actions">
|
||||
<woot-button
|
||||
class="new-message"
|
||||
size="small expanded"
|
||||
@click="toggleConversationModal"
|
||||
>
|
||||
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small expanded"
|
||||
@click="toggleEditModal"
|
||||
>
|
||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<div v-if="!showNewMessage">
|
||||
<div>
|
||||
<woot-button
|
||||
class="edit-contact"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="toggleEditModal"
|
||||
>
|
||||
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="isAdmin">
|
||||
<woot-button
|
||||
class="delete-contact"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="alert"
|
||||
@click="toggleDeleteModal"
|
||||
:disabled="uiFlags.isDeleting"
|
||||
>
|
||||
{{ $t('DELETE_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="contact-actions">
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
class="new-message"
|
||||
icon="ion-chatboxes"
|
||||
size="small expanded"
|
||||
@click="toggleConversationModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
class="edit-contact"
|
||||
icon="ion-edit"
|
||||
variant="smooth"
|
||||
size="small expanded"
|
||||
@click="toggleEditModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
class="delete-contact"
|
||||
icon="ion-trash-a"
|
||||
variant="hollow"
|
||||
size="small expanded"
|
||||
color-scheme="alert"
|
||||
@click="toggleDeleteModal"
|
||||
:disabled="uiFlags.isDeleting"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<edit-contact
|
||||
v-if="showEditModal"
|
||||
|
@ -80,11 +109,24 @@
|
|||
@cancel="toggleEditModal"
|
||||
/>
|
||||
<new-conversation
|
||||
v-if="contact.id"
|
||||
:show="showConversationModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleConversationModal"
|
||||
/>
|
||||
</div>
|
||||
<woot-confirm-delete-modal
|
||||
v-if="showDeleteModal"
|
||||
:show.sync="showDeleteModal"
|
||||
:title="$t('DELETE_CONTACT.CONFIRM.TITLE')"
|
||||
:message="confirmDeleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
:confirm-value="contact.name"
|
||||
:confirm-place-holder-text="confirmPlaceHolderText"
|
||||
@on-confirm="confirmDeletion"
|
||||
@on-close="closeDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -93,6 +135,9 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
|||
import SocialIcons from './SocialIcons';
|
||||
import EditContact from './EditContact';
|
||||
import NewConversation from './NewConversation';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import adminMixin from '../../../../mixins/isAdmin';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -102,6 +147,7 @@ export default {
|
|||
SocialIcons,
|
||||
NewConversation,
|
||||
},
|
||||
mixins: [alertMixin, adminMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
|
@ -120,9 +166,11 @@ export default {
|
|||
return {
|
||||
showEditModal: false,
|
||||
showConversationModal: false,
|
||||
showDeleteModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
|
@ -134,6 +182,23 @@ export default {
|
|||
|
||||
return { twitter: twitterScreenName, ...(socialProfiles || {}) };
|
||||
},
|
||||
// Delete Modal
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.YES')} ${this.contact.name}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.NO')} ${this.contact.name}`;
|
||||
},
|
||||
confirmDeleteMessage() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.MESSAGE')} ${
|
||||
this.contact.name
|
||||
} ?`;
|
||||
},
|
||||
confirmPlaceHolderText() {
|
||||
return `${this.$t('DELETE_CONTACT.CONFIRM.PLACE_HOLDER', {
|
||||
contactName: this.contact.name,
|
||||
})}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleEditModal() {
|
||||
|
@ -142,6 +207,31 @@ export default {
|
|||
toggleConversationModal() {
|
||||
this.showConversationModal = !this.showConversationModal;
|
||||
},
|
||||
toggleDeleteModal() {
|
||||
this.showDeleteModal = !this.showDeleteModal;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.deleteContact(this.contact);
|
||||
this.closeDelete();
|
||||
},
|
||||
closeDelete() {
|
||||
this.showDeleteModal = false;
|
||||
this.showConversationModal = false;
|
||||
this.showEditModal = false;
|
||||
},
|
||||
async deleteContact({ id }) {
|
||||
try {
|
||||
await this.$store.dispatch('contacts/delete', id);
|
||||
this.$emit('panel-close');
|
||||
this.showAlert(this.$t('DELETE_CONTACT.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message
|
||||
? error.message
|
||||
: this.$t('DELETE_CONTACT.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -179,17 +269,32 @@ export default {
|
|||
.contact-actions {
|
||||
margin-top: var(--space-small);
|
||||
}
|
||||
.button.edit-contact {
|
||||
|
||||
.edit-contact {
|
||||
margin-left: var(--space-medium);
|
||||
}
|
||||
|
||||
.button.new-message {
|
||||
margin-right: var(--space-small);
|
||||
.delete-contact {
|
||||
margin-left: var(--space-medium);
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.new-message {
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
|
||||
.edit-contact {
|
||||
margin-left: var(--space-small);
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
|
||||
.delete-contact {
|
||||
margin-left: var(--space-small);
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -237,7 +237,7 @@ export default {
|
|||
if (this.isOngoingType) {
|
||||
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
||||
}
|
||||
return this.$store.getters['inboxes/getTwilioInboxes'];
|
||||
return this.$store.getters['inboxes/getTwilioSMSInboxes'];
|
||||
},
|
||||
sendersAndBotList() {
|
||||
return [
|
||||
|
|
|
@ -161,7 +161,7 @@ export default {
|
|||
if (this.isOngoingType) {
|
||||
return this.$store.getters['inboxes/getWebsiteInboxes'];
|
||||
}
|
||||
return this.$store.getters['inboxes/getTwilioInboxes'];
|
||||
return this.$store.getters['inboxes/getTwilioSMSInboxes'];
|
||||
},
|
||||
pageTitle() {
|
||||
return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${
|
||||
|
|
|
@ -88,7 +88,7 @@ export default {
|
|||
const selectedAgents = this.selectedAgents.map(x => x.id);
|
||||
|
||||
try {
|
||||
await InboxMembersAPI.create({ inboxId, agentList: selectedAgents });
|
||||
await InboxMembersAPI.update({ inboxId, agentList: selectedAgents });
|
||||
router.replace({
|
||||
name: 'settings_inbox_finish',
|
||||
params: {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<woot-code
|
||||
v-if="isATwilioInbox"
|
||||
lang="html"
|
||||
:script="currentInbox.webhook_url"
|
||||
:script="currentInbox.callback_webhook_url"
|
||||
>
|
||||
</woot-code>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<woot-code
|
||||
v-if="isALineInbox"
|
||||
lang="html"
|
||||
:script="currentInbox.webhook_url"
|
||||
:script="currentInbox.callback_webhook_url"
|
||||
>
|
||||
</woot-code>
|
||||
</div>
|
||||
|
@ -93,6 +93,12 @@ export default {
|
|||
)}`;
|
||||
}
|
||||
|
||||
if (this.isALineInbox) {
|
||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||
'INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE'
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.isAEmailInbox) {
|
||||
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
||||
}
|
||||
|
|
|
@ -51,6 +51,9 @@
|
|||
<span v-if="item.channel_type === 'Channel::Telegram'">
|
||||
Telegram
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Line'">
|
||||
Line
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Api'">
|
||||
{{ globalConfig.apiChannelName || 'API' }}
|
||||
</span>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<woot-avatar-uploader
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AVATAR.LABEL')"
|
||||
:src="avatarUrl"
|
||||
deleteAvatar
|
||||
delete-avatar
|
||||
@change="handleImageUpload"
|
||||
@onAvatarDelete="handleAvatarDelete"
|
||||
/>
|
||||
|
@ -32,6 +32,24 @@
|
|||
:label="inboxNameLabel"
|
||||
:placeholder="inboxNamePlaceHolder"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAPIInbox"
|
||||
v-model.trim="webhookUrl"
|
||||
class="medium-9 columns"
|
||||
:class="{ error: $v.webhookUrl.$error }"
|
||||
:label="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.PLACEHOLDER')
|
||||
"
|
||||
:error="
|
||||
$v.webhookUrl.$error
|
||||
? $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WEBHOOK_URL.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.webhookUrl.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model.trim="channelWebsiteUrl"
|
||||
|
@ -212,6 +230,16 @@
|
|||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
v-if="isAPIInbox"
|
||||
type="submit"
|
||||
:disabled="$v.webhookUrl.$invalid"
|
||||
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||
:loading="uiFlags.isUpdatingInbox"
|
||||
@click="updateInbox"
|
||||
/>
|
||||
<woot-submit-button
|
||||
v-else
|
||||
type="submit"
|
||||
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||
:loading="uiFlags.isUpdatingInbox"
|
||||
@click="updateInbox"
|
||||
|
@ -259,7 +287,21 @@
|
|||
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
|
||||
>
|
||||
<woot-code :script="twilioCallbackURL" lang="html"></woot-code>
|
||||
<woot-code
|
||||
:script="inbox.callback_webhook_url"
|
||||
lang="html"
|
||||
></woot-code>
|
||||
</settings-section>
|
||||
</div>
|
||||
<div v-else-if="isALineChannel" class="settings--content">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE')"
|
||||
>
|
||||
<woot-code
|
||||
:script="inbox.callback_webhook_url"
|
||||
lang="html"
|
||||
></woot-code>
|
||||
</settings-section>
|
||||
</div>
|
||||
<div v-else-if="isAWebWidgetInbox">
|
||||
|
@ -310,6 +352,8 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import { shouldBeUrl } from 'shared/helpers/Validators';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner';
|
||||
|
@ -343,6 +387,7 @@ export default {
|
|||
csatSurveyEnabled: false,
|
||||
selectedInboxName: '',
|
||||
channelWebsiteUrl: '',
|
||||
webhookUrl: '',
|
||||
channelWelcomeTitle: '',
|
||||
channelWelcomeTagline: '',
|
||||
selectedFeatureFlags: [],
|
||||
|
@ -378,6 +423,10 @@ export default {
|
|||
key: 'collaborators',
|
||||
name: this.$t('INBOX_MGMT.TABS.COLLABORATORS'),
|
||||
},
|
||||
{
|
||||
key: 'businesshours',
|
||||
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.isAWebWidgetInbox) {
|
||||
|
@ -387,10 +436,6 @@ export default {
|
|||
key: 'preChatForm',
|
||||
name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'),
|
||||
},
|
||||
{
|
||||
key: 'businesshours',
|
||||
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
|
||||
},
|
||||
{
|
||||
key: 'configuration',
|
||||
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
|
||||
|
@ -398,7 +443,12 @@ export default {
|
|||
];
|
||||
}
|
||||
|
||||
if (this.isATwilioChannel || this.isAPIInbox || this.isAnEmailChannel) {
|
||||
if (
|
||||
this.isATwilioChannel ||
|
||||
this.isALineChannel ||
|
||||
this.isAPIInbox ||
|
||||
this.isAnEmailChannel
|
||||
) {
|
||||
return [
|
||||
...visibleToAllChannelTabs,
|
||||
{
|
||||
|
@ -484,6 +534,7 @@ export default {
|
|||
this.fetchAttachedAgents();
|
||||
this.avatarUrl = this.inbox.avatar_url;
|
||||
this.selectedInboxName = this.inbox.name;
|
||||
this.webhookUrl = this.inbox.webhook_url;
|
||||
this.greetingEnabled = this.inbox.greeting_enabled || false;
|
||||
this.greetingMessage = this.inbox.greeting_message || '';
|
||||
this.autoAssignment = this.inbox.enable_auto_assignment;
|
||||
|
@ -536,6 +587,7 @@ export default {
|
|||
channel: {
|
||||
widget_color: this.inbox.widget_color,
|
||||
website_url: this.channelWebsiteUrl,
|
||||
webhook_url: this.webhookUrl,
|
||||
welcome_title: this.channelWelcomeTitle || '',
|
||||
welcome_tagline: this.channelWelcomeTagline || '',
|
||||
selectedFeatureFlags: this.selectedFeatureFlags,
|
||||
|
@ -574,6 +626,10 @@ export default {
|
|||
},
|
||||
},
|
||||
validations: {
|
||||
webhookUrl: {
|
||||
required,
|
||||
shouldBeUrl,
|
||||
},
|
||||
selectedAgents: {
|
||||
isEmpty() {
|
||||
return !!this.selectedAgents.length;
|
||||
|
|
|
@ -82,6 +82,9 @@ export const mutations = {
|
|||
const conversations = $state.records[id] || [];
|
||||
Vue.set($state.records, id, [...conversations, data]);
|
||||
},
|
||||
[types.default.DELETE_CONTACT_CONVERSATION]: ($state, id) => {
|
||||
Vue.delete($state.records, id);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -82,6 +82,32 @@ export const actions = {
|
|||
}
|
||||
}
|
||||
},
|
||||
import: async ({ commit }, file) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
await ContactAPI.importContacts(file);
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
|
||||
if (error.response?.data?.message) {
|
||||
throw new ExceptionWithMessage(error.response.data.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
delete: async ({ commit }, id) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await ContactAPI.delete(id);
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: false });
|
||||
if (error.response?.data?.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
fetchContactableInbox: async ({ commit }, id) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
|
||||
|
@ -110,4 +136,12 @@ export const actions = {
|
|||
setContact({ commit }, data) {
|
||||
commit(types.SET_CONTACT_ITEM, data);
|
||||
},
|
||||
|
||||
deleteContactThroughConversations: ({ commit }, id) => {
|
||||
commit(types.DELETE_CONTACT, id);
|
||||
commit(types.CLEAR_CONTACT_CONVERSATIONS, id, { root: true });
|
||||
commit(`contactConversations/${types.DELETE_CONTACT_CONVERSATION}`, id, {
|
||||
root: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ const state = {
|
|||
isFetchingItem: false,
|
||||
isFetchingInboxes: false,
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
sortOrder: [],
|
||||
};
|
||||
|
|
|
@ -46,6 +46,12 @@ export const mutations = {
|
|||
Vue.set($state.records, data.id, data);
|
||||
},
|
||||
|
||||
[types.DELETE_CONTACT]: ($state, id) => {
|
||||
const index = $state.sortOrder.findIndex(item => item === id);
|
||||
Vue.delete($state.sortOrder, index);
|
||||
Vue.delete($state.records, id);
|
||||
},
|
||||
|
||||
[types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
|
||||
Object.values($state.records).forEach(element => {
|
||||
const availabilityStatus = data[element.id];
|
||||
|
|
|
@ -155,6 +155,7 @@ const actions = {
|
|||
},
|
||||
|
||||
sendMessage: async ({ commit }, data) => {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
const pendingMessage = createPendingMessage(data);
|
||||
commit(types.default.ADD_MESSAGE, pendingMessage);
|
||||
|
@ -164,7 +165,7 @@ const actions = {
|
|||
status: MESSAGE_STATUS.SENT,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -177,6 +177,13 @@ export const mutations = {
|
|||
Vue.set(chat, 'can_reply', canReply);
|
||||
}
|
||||
},
|
||||
|
||||
[types.default.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) {
|
||||
const chats = _state.allConversations.filter(
|
||||
c => c.meta.sender.id !== contactId
|
||||
);
|
||||
Vue.set(_state, 'allConversations', chats);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -73,6 +73,11 @@ export const getters = {
|
|||
item => item.channel_type === INBOX_TYPES.TWILIO
|
||||
);
|
||||
},
|
||||
getTwilioSMSInboxes($state) {
|
||||
return $state.records.filter(
|
||||
item => item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
|
|
@ -139,6 +139,27 @@ describe('#actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('sends correct mutations if API is success', async () => {
|
||||
axios.delete.mockResolvedValue();
|
||||
await actions.delete({ commit }, contactList[0].id);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: true }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.delete({ commit }, contactList[0].id)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: true }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setContact', () => {
|
||||
it('returns correct mutations', () => {
|
||||
const data = { id: 1, name: 'john doe', availability_status: 'online' };
|
||||
|
@ -146,4 +167,19 @@ describe('#actions', () => {
|
|||
expect(commit.mock.calls).toEqual([[types.SET_CONTACT_ITEM, data]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteContactThroughConversations', () => {
|
||||
it('returns correct mutations', () => {
|
||||
actions.deleteContactThroughConversations({ commit }, contactList[0].id);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.DELETE_CONTACT, contactList[0].id],
|
||||
[types.CLEAR_CONTACT_CONVERSATIONS, contactList[0].id, { root: true }],
|
||||
[
|
||||
`contactConversations/${types.DELETE_CONTACT_CONVERSATION}`,
|
||||
contactList[0].id,
|
||||
{ root: true },
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -49,10 +49,10 @@ export default [
|
|||
name: 'Test Widget 5',
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
avatar_url: null,
|
||||
medium: 'sms',
|
||||
page_id: null,
|
||||
widget_color: '#68BC00',
|
||||
website_token: 'randomid125',
|
||||
enable_auto_assignment: true,
|
||||
},
|
||||
|
||||
];
|
||||
|
|
|
@ -19,6 +19,11 @@ describe('#getters', () => {
|
|||
expect(getters.getTwilioInboxes(state).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('getTwilioSMSInboxes', () => {
|
||||
const state = { records: inboxList };
|
||||
expect(getters.getTwilioSMSInboxes(state).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('getInbox', () => {
|
||||
const state = {
|
||||
records: inboxList,
|
||||
|
|
|
@ -18,6 +18,7 @@ export default {
|
|||
CHANGE_CHAT_STATUS_FILTER: 'CHANGE_CHAT_STATUS_FILTER',
|
||||
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
|
||||
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
|
||||
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
|
||||
|
||||
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
|
||||
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
|
||||
|
@ -101,6 +102,7 @@ export default {
|
|||
SET_CONTACTS: 'SET_CONTACTS',
|
||||
CLEAR_CONTACTS: 'CLEAR_CONTACTS',
|
||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||
DELETE_CONTACT: 'DELETE_CONTACT',
|
||||
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||
|
||||
// Notifications
|
||||
|
@ -119,6 +121,7 @@ export default {
|
|||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
||||
ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION',
|
||||
DELETE_CONTACT_CONVERSATION: 'DELETE_CONTACT_CONVERSATION',
|
||||
|
||||
// Contact Label
|
||||
SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG',
|
||||
|
|
|
@ -40,8 +40,8 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
|||
launcherTitle: chatwootSettings.launcherTitle || '',
|
||||
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
||||
|
||||
toggle() {
|
||||
IFrameHelper.events.toggleBubble();
|
||||
toggle(state) {
|
||||
IFrameHelper.events.toggleBubble(state);
|
||||
},
|
||||
|
||||
setUser(identifier, user) {
|
||||
|
|
|
@ -148,8 +148,15 @@ export const IFrameHelper = {
|
|||
setBubbleText(window.$chatwoot.launcherTitle || message.label);
|
||||
},
|
||||
|
||||
toggleBubble: () => {
|
||||
onBubbleClick();
|
||||
toggleBubble: state => {
|
||||
let bubbleState = {};
|
||||
if (state === 'open') {
|
||||
bubbleState.toggleValue = true;
|
||||
} else if (state === 'close') {
|
||||
bubbleState.toggleValue = false;
|
||||
}
|
||||
|
||||
onBubbleClick(bubbleState);
|
||||
},
|
||||
|
||||
onBubbleToggle: isOpen => {
|
||||
|
|
|
@ -99,6 +99,7 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
.woot--close::before, .woot--close::after {
|
||||
background-color: #fff;
|
||||
content: ' ';
|
||||
display: inline;
|
||||
height: 24px;
|
||||
left: 32px;
|
||||
position: absolute;
|
||||
|
|
|
@ -41,10 +41,11 @@ export default {
|
|||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.option {
|
||||
border: 1px solid $color-woot;
|
||||
border-radius: $space-jumbo;
|
||||
border: 1px solid $color-woot;
|
||||
float: left;
|
||||
margin: $space-smaller;
|
||||
max-width: 100%;
|
||||
|
||||
.option-button {
|
||||
background: transparent;
|
||||
|
@ -52,7 +53,11 @@ export default {
|
|||
border: 0;
|
||||
color: $color-woot;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
min-height: $space-two * 2;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
:src="selectedItem.thumbnail"
|
||||
size="24px"
|
||||
:status="selectedItem.availability_status"
|
||||
:badge="selectedItem.channel"
|
||||
:username="selectedItem.name"
|
||||
/>
|
||||
<div class="selector-name-wrap">
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/);
|
||||
export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === '';
|
||||
export const shouldBeUrl = (value = '') =>
|
||||
value ? value.startsWith('http') : true;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { shouldBeUrl } from '../Validators';
|
||||
|
||||
describe('#shouldBeUrl', () => {
|
||||
it('should return correct url', () => {
|
||||
expect(shouldBeUrl('http')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -1,8 +1,28 @@
|
|||
import { isEscape } from '../helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyEvents);
|
||||
document.addEventListener('keydown', this.onKeyDownHandler);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onKeyDownHandler);
|
||||
},
|
||||
methods: {
|
||||
onKeyDownHandler(e) {
|
||||
const isEventFromAnInputBox =
|
||||
e.target?.tagName === 'INPUT' || e.target?.tagName === 'TEXTAREA';
|
||||
const isEventFromProseMirror = e.target?.className?.includes(
|
||||
'ProseMirror'
|
||||
);
|
||||
|
||||
if (isEventFromAnInputBox || isEventFromProseMirror) {
|
||||
if (isEscape(e)) {
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleKeyEvents(e);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@ export const INBOX_TYPES = {
|
|||
TWILIO: 'Channel::TwilioSms',
|
||||
API: 'Channel::Api',
|
||||
EMAIL: 'Channel::Email',
|
||||
TELEGRAM: 'Channel::Telegram',
|
||||
LINE: 'Channel::Line',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -27,16 +29,41 @@ export default {
|
|||
isATwilioChannel() {
|
||||
return this.channelType === INBOX_TYPES.TWILIO;
|
||||
},
|
||||
isALineChannel() {
|
||||
return this.channelType === INBOX_TYPES.LINE;
|
||||
},
|
||||
isAnEmailChannel() {
|
||||
return this.channelType === INBOX_TYPES.EMAIL;
|
||||
},
|
||||
isATwilioSMSChannel() {
|
||||
const { phone_number: phoneNumber = '' } = this.inbox;
|
||||
return this.isATwilioChannel && !phoneNumber.startsWith('whatsapp');
|
||||
const { medium: medium = '' } = this.inbox;
|
||||
return this.isATwilioChannel && medium === 'sms';
|
||||
},
|
||||
isATwilioWhatsappChannel() {
|
||||
const { phone_number: phoneNumber = '' } = this.inbox;
|
||||
return this.isATwilioChannel && phoneNumber.startsWith('whatsapp');
|
||||
const { medium: medium = '' } = this.inbox;
|
||||
return this.isATwilioChannel && medium === 'whatsapp';
|
||||
},
|
||||
isTwitterInboxTweet() {
|
||||
return (
|
||||
this.chat &&
|
||||
this.chat.additional_attributes &&
|
||||
this.chat.additional_attributes.type === 'tweet'
|
||||
);
|
||||
},
|
||||
twilioBadge() {
|
||||
return `${this.isATwilioSMSChannel ? 'sms' : 'whatsapp'}`;
|
||||
},
|
||||
twitterBadge() {
|
||||
return `${this.isTwitterInboxTweet ? 'twitter-tweet' : 'twitter-chat'}`;
|
||||
},
|
||||
inboxBadge() {
|
||||
if (this.isATwitterInbox) {
|
||||
return this.twitterBadge;
|
||||
}
|
||||
if (this.isATwilioChannel) {
|
||||
return this.twilioBadge;
|
||||
}
|
||||
return this.channelType;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -70,7 +70,23 @@ describe('inboxMixin', () => {
|
|||
return {
|
||||
inbox: {
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
phone_number: '+91944444444',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isATwilioChannel).toBe(true);
|
||||
});
|
||||
|
||||
it('isATwilioSMSChannel returns true if channel type is Twilio and medium is SMS', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [inboxMixin],
|
||||
data() {
|
||||
return {
|
||||
inbox: {
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
medium: 'sms',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -80,7 +96,7 @@ describe('inboxMixin', () => {
|
|||
expect(wrapper.vm.isATwilioSMSChannel).toBe(true);
|
||||
});
|
||||
|
||||
it('isATwilioWhatsappChannel returns true if channel type is Twilio and phonenumber is a whatsapp number', () => {
|
||||
it('isATwilioWhatsappChannel returns true if channel type is Twilio and medium is whatsapp', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [inboxMixin],
|
||||
|
@ -88,7 +104,7 @@ describe('inboxMixin', () => {
|
|||
return {
|
||||
inbox: {
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
phone_number: 'whatsapp:+91944444444',
|
||||
medium: 'whatsapp',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -111,4 +127,79 @@ describe('inboxMixin', () => {
|
|||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isAnEmailChannel).toBe(true);
|
||||
});
|
||||
|
||||
it('isTwitterInboxTweet returns true if Twitter channel type is tweet', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [inboxMixin],
|
||||
data() {
|
||||
return {
|
||||
chat: {
|
||||
channel_type: 'Channel::TwitterProfile',
|
||||
additional_attributes: {
|
||||
type: 'tweet',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isTwitterInboxTweet).toBe(true);
|
||||
});
|
||||
|
||||
it('twilioBadge returns string sms if channel type is Twilio and medium is sms', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [inboxMixin],
|
||||
data() {
|
||||
return {
|
||||
inbox: {
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
medium: 'sms',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isATwilioSMSChannel).toBe(true);
|
||||
expect(wrapper.vm.twilioBadge).toBe('sms');
|
||||
});
|
||||
|
||||
it('twitterBadge returns string twitter-tweet if Twitter channel type is tweet', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [inboxMixin],
|
||||
data() {
|
||||
return {
|
||||
chat: {
|
||||
id: 1,
|
||||
additional_attributes: {
|
||||
type: 'tweet',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isTwitterInboxTweet).toBe(true);
|
||||
expect(wrapper.vm.twitterBadge).toBe('twitter-tweet');
|
||||
});
|
||||
|
||||
it('inboxBadge returns string Channel::Telegram if isATwilioChannel and isATwitterInbox is false', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [inboxMixin],
|
||||
data() {
|
||||
return {
|
||||
inbox: {
|
||||
channel_type: 'Channel::Telegram',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isATwilioChannel).toBe(false);
|
||||
expect(wrapper.vm.isATwitterInbox).toBe(false);
|
||||
expect(wrapper.vm.channelType).toBe('Channel::Telegram');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ class ContactIpLookupJob < ApplicationJob
|
|||
geocoder_result = Geocoder.search(ip).first
|
||||
return unless geocoder_result
|
||||
|
||||
contact.additional_attributes ||= {}
|
||||
contact.additional_attributes['city'] = geocoder_result.city
|
||||
contact.additional_attributes['country'] = geocoder_result.country
|
||||
contact.additional_attributes['country_code'] = geocoder_result.country_code
|
||||
|
@ -34,7 +35,7 @@ class ContactIpLookupJob < ApplicationJob
|
|||
end
|
||||
|
||||
def get_contact_ip(contact)
|
||||
contact.additional_attributes['updated_at_ip'] || contact.additional_attributes['created_at_ip']
|
||||
contact.additional_attributes&.dig('updated_at_ip') || contact.additional_attributes&.dig('created_at_ip')
|
||||
end
|
||||
|
||||
def ensure_look_up_db
|
||||
|
|
11
app/jobs/labels/update_job.rb
Normal file
11
app/jobs/labels/update_job.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
class Labels::UpdateJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(new_label_title, old_label_title, account_id)
|
||||
Labels::UpdateService.new(
|
||||
new_label_title: new_label_title,
|
||||
old_label_title: old_label_title,
|
||||
account_id: account_id
|
||||
).perform
|
||||
end
|
||||
end
|
|
@ -111,6 +111,13 @@ class ActionCableListener < BaseListener
|
|||
broadcast(account, tokens, CONTACT_MERGED, contact.push_event_data)
|
||||
end
|
||||
|
||||
def contact_deleted(event)
|
||||
contact, account = extract_contact_and_account(event)
|
||||
tokens = user_tokens(account, account.agents)
|
||||
|
||||
broadcast(account, tokens, CONTACT_DELETED, contact.push_event_data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def typing_event_listener_tokens(account, conversation, user)
|
||||
|
|
|
@ -22,7 +22,7 @@ class ApplicationMailbox < ActionMailbox::Base
|
|||
proc do |inbound_mail_obj|
|
||||
is_a_support_email = false
|
||||
inbound_mail_obj.mail.to&.each do |email|
|
||||
channel = Channel::Email.find_by('email = ? OR forward_to_email = ?', email, email)
|
||||
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase)
|
||||
if channel.present?
|
||||
is_a_support_email = true
|
||||
break
|
||||
|
|
|
@ -21,7 +21,7 @@ class SupportMailbox < ApplicationMailbox
|
|||
|
||||
def find_channel
|
||||
mail.to.each do |email|
|
||||
@channel = Channel::Email.find_by('email = ? OR forward_to_email = ?', email, email)
|
||||
@channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase)
|
||||
break if @channel.present?
|
||||
end
|
||||
raise 'Email channel/inbox not found' if @channel.nil?
|
||||
|
@ -82,6 +82,6 @@ class SupportMailbox < ApplicationMailbox
|
|||
end
|
||||
|
||||
def identify_contact_name
|
||||
processed_mail.from.first.split('@').first
|
||||
processed_mail.sender_name || processed_mail.from.first.split('@').first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,7 @@ class Attachment < ApplicationRecord
|
|||
enum file_type: [:image, :audio, :video, :file, :location, :fallback]
|
||||
|
||||
def push_event_data
|
||||
return unless file_type
|
||||
return base_data.merge(location_metadata) if file_type.to_sym == :location
|
||||
return base_data.merge(fallback_data) if file_type.to_sym == :fallback
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ class Contact < ApplicationRecord
|
|||
before_validation :prepare_email_attribute
|
||||
after_create_commit :dispatch_create_event, :ip_lookup
|
||||
after_update_commit :dispatch_update_event
|
||||
after_destroy_commit :dispatch_destroy_event
|
||||
|
||||
def get_source_id(inbox_id)
|
||||
contact_inboxes.find_by!(inbox_id: inbox_id).source_id
|
||||
|
@ -73,7 +74,8 @@ class Contact < ApplicationRecord
|
|||
id: id,
|
||||
name: name,
|
||||
avatar: avatar_url,
|
||||
type: 'contact'
|
||||
type: 'contact',
|
||||
account: account.webhook_data
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -98,4 +100,8 @@ class Contact < ApplicationRecord
|
|||
def dispatch_update_event
|
||||
Rails.configuration.dispatcher.dispatch(CONTACT_UPDATED, Time.zone.now, contact: self)
|
||||
end
|
||||
|
||||
def dispatch_destroy_event
|
||||
Rails.configuration.dispatcher.dispatch(CONTACT_DELETED, Time.zone.now, contact: self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
# id :integer not null, primary key
|
||||
# additional_attributes :jsonb
|
||||
# agent_last_seen_at :datetime
|
||||
# assignee_last_seen_at :datetime
|
||||
# contact_last_seen_at :datetime
|
||||
# custom_attributes :jsonb
|
||||
# identifier :string
|
||||
# last_activity_at :datetime not null
|
||||
# snoozed_until :datetime
|
||||
|
|
|
@ -31,6 +31,7 @@ class Inbox < ApplicationRecord
|
|||
include Avatarable
|
||||
include OutOfOffisable
|
||||
|
||||
validates :name, presence: true
|
||||
validates :account_id, presence: true
|
||||
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }
|
||||
|
||||
|
@ -82,6 +83,10 @@ class Inbox < ApplicationRecord
|
|||
channel_type == 'Channel::Email'
|
||||
end
|
||||
|
||||
def twilio?
|
||||
channel_type == 'Channel::TwilioSms'
|
||||
end
|
||||
|
||||
def inbox_type
|
||||
channel.name
|
||||
end
|
||||
|
@ -93,9 +98,9 @@ class Inbox < ApplicationRecord
|
|||
}
|
||||
end
|
||||
|
||||
def webhook_url
|
||||
def callback_webhook_url
|
||||
case channel_type
|
||||
when 'Channel::TwilioSMS'
|
||||
when 'Channel::TwilioSms'
|
||||
"#{ENV['FRONTEND_URL']}/twilio/callback"
|
||||
when 'Channel::Line'
|
||||
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"
|
||||
|
|
|
@ -25,6 +25,8 @@ class Label < ApplicationRecord
|
|||
format: { with: UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE },
|
||||
uniqueness: { scope: :account_id }
|
||||
|
||||
after_update_commit :update_associated_models
|
||||
|
||||
before_validation do
|
||||
self.title = title.downcase if attribute_present?('title')
|
||||
end
|
||||
|
@ -40,4 +42,12 @@ class Label < ApplicationRecord
|
|||
def events
|
||||
account.events.where(conversation_id: conversations.pluck(:id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_associated_models
|
||||
return unless title_previously_changed?
|
||||
|
||||
Labels::UpdateJob.perform_later(title, title_previously_was, account_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,4 +40,12 @@ class Team < ApplicationRecord
|
|||
def remove_member(user_id)
|
||||
team_members.find_by(user_id: user_id)&.destroy
|
||||
end
|
||||
|
||||
def messages
|
||||
account.messages.where(conversation_id: conversations.pluck(:id))
|
||||
end
|
||||
|
||||
def events
|
||||
account.events.where(conversation_id: conversations.pluck(:id))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,10 +43,10 @@ class WorkingHour < ApplicationRecord
|
|||
def open_at?(time)
|
||||
return false if closed_all_day?
|
||||
|
||||
time.hour >= open_hour &&
|
||||
time.min >= open_minutes &&
|
||||
time.hour <= close_hour &&
|
||||
time.min <= close_minutes
|
||||
open_time = Time.zone.now.in_time_zone(inbox.timezone).change({ hour: open_hour, min: open_minutes })
|
||||
close_time = Time.zone.now.in_time_zone(inbox.timezone).change({ hour: close_hour, min: close_minutes })
|
||||
|
||||
time.between?(open_time, close_time)
|
||||
end
|
||||
|
||||
def open_now?
|
||||
|
|
|
@ -30,4 +30,8 @@ class ContactPolicy < ApplicationPolicy
|
|||
def create?
|
||||
true
|
||||
end
|
||||
|
||||
def destroy?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,6 +82,10 @@ class MailPresenter < SimpleDelegator
|
|||
@mail.from.map(&:downcase)
|
||||
end
|
||||
|
||||
def sender_name
|
||||
Mail::Address.new(@mail[:from].value).name
|
||||
end
|
||||
|
||||
def original_sender
|
||||
@mail['X-Original-Sender'].try(:value) || from.first
|
||||
end
|
||||
|
|
35
app/services/labels/update_service.rb
Normal file
35
app/services/labels/update_service.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
class Labels::UpdateService
|
||||
pattr_initialize [:new_label_title!, :old_label_title!, :account_id!]
|
||||
|
||||
def perform
|
||||
tagged_conversations.find_in_batches do |conversation_batch|
|
||||
conversation_batch.each do |conversation|
|
||||
conversation.label_list.remove(old_label_title)
|
||||
conversation.label_list.add(new_label_title)
|
||||
conversation.save!
|
||||
end
|
||||
end
|
||||
|
||||
tagged_contacts.find_in_batches do |contact_batch|
|
||||
contact_batch.each do |contact|
|
||||
contact.label_list.remove(old_label_title)
|
||||
contact.label_list.add(new_label_title)
|
||||
contact.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tagged_conversations
|
||||
account.conversations.tagged_with(old_label_title)
|
||||
end
|
||||
|
||||
def tagged_contacts
|
||||
account.contacts.tagged_with(old_label_title)
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
end
|
||||
end
|
|
@ -1,9 +1,17 @@
|
|||
# ref : https://developers.line.biz/en/docs/messaging-api/receiving-messages/#webhook-event-types
|
||||
# https://developers.line.biz/en/reference/messaging-api/#message-event
|
||||
|
||||
class Line::IncomingMessageService
|
||||
include ::FileTypeHelper
|
||||
pattr_initialize [:inbox!, :params!]
|
||||
|
||||
def perform
|
||||
# probably test events
|
||||
return if params[:events].blank?
|
||||
|
||||
line_contact_info
|
||||
return if line_contact_info['userId'].blank?
|
||||
|
||||
set_contact
|
||||
set_conversation
|
||||
# TODO: iterate over the events and handle the attachments in future
|
||||
|
|
|
@ -14,14 +14,18 @@ class MessageTemplates::HookExecutionService
|
|||
delegate :contact, to: :conversation
|
||||
|
||||
def trigger_templates
|
||||
# TODO: let's see whether this is needed and remove this and related logic if not
|
||||
# ::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message?
|
||||
::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message?
|
||||
::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting?
|
||||
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if inbox.enable_email_collect && should_send_email_collect?
|
||||
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform if should_send_csat_survey?
|
||||
end
|
||||
|
||||
def should_send_out_of_office_message?
|
||||
# should not send if its a tweet message
|
||||
return false if conversation.tweet?
|
||||
# should not send for outbound messages
|
||||
return false unless message.incoming?
|
||||
|
||||
inbox.out_of_office? && conversation.messages.today.template.empty? && inbox.out_of_office_message.present?
|
||||
end
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue