Merge branch 'chore/upgrade-to-postcss-8' of https://github.com/chatwoot/chatwoot into feat/widget-multi-getters

This commit is contained in:
Nithin David 2021-09-29 17:34:50 +05:30
commit 7bdfb5b075
151 changed files with 2383 additions and 241 deletions

3
.bundler-audit.yml Normal file
View file

@ -0,0 +1,3 @@
---
ignore:
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)

View file

@ -56,8 +56,7 @@ gem 'activerecord-import'
gem 'dotenv-rails' gem 'dotenv-rails'
gem 'foreman' gem 'foreman'
gem 'puma' gem 'puma'
gem 'rack-timeout' gem 'webpacker', '~> 5.4.0'
gem 'webpacker', '~> 5.x'
# metrics on heroku # metrics on heroku
gem 'barnes' gem 'barnes'
@ -122,6 +121,11 @@ gem 'hairtrigger'
gem 'procore-sift' 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 group :development do
gem 'annotate' gem 'annotate'
gem 'bullet' gem 'bullet'

View file

@ -247,6 +247,7 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
google-protobuf (3.17.3)
google-protobuf (3.17.3-universal-darwin) google-protobuf (3.17.3-universal-darwin)
google-protobuf (3.17.3-x86_64-linux) google-protobuf (3.17.3-x86_64-linux)
googleapis-common-protos (1.3.11) googleapis-common-protos (1.3.11)
@ -264,6 +265,9 @@ GEM
signet (~> 0.14) signet (~> 0.14)
groupdate (5.2.2) groupdate (5.2.2)
activesupport (>= 5) activesupport (>= 5)
grpc (1.38.0)
google-protobuf (~> 3.15)
googleapis-common-protos-types (~> 1.0)
grpc (1.38.0-universal-darwin) grpc (1.38.0-universal-darwin)
google-protobuf (~> 3.15) google-protobuf (~> 3.15)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
@ -346,6 +350,7 @@ GEM
mime-types-data (3.2021.0704) mime-types-data (3.2021.0704)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.1) mini_mime (1.1.1)
mini_portile2 (2.5.3)
minitest (5.14.4) minitest (5.14.4)
mock_redis (0.28.0) mock_redis (0.28.0)
ruby2_keywords ruby2_keywords
@ -360,6 +365,9 @@ GEM
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (7.2.0) newrelic_rpm (7.2.0)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogiri (1.11.7-arm64-darwin) nokogiri (1.11.7-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.11.7-x86_64-darwin) nokogiri (1.11.7-x86_64-darwin)
@ -617,6 +625,7 @@ GEM
PLATFORMS PLATFORMS
arm64-darwin-20 arm64-darwin-20
ruby
x86_64-darwin-18 x86_64-darwin-18
x86_64-darwin-20 x86_64-darwin-20
x86_64-darwin-21 x86_64-darwin-21
@ -714,7 +723,7 @@ DEPENDENCIES
valid_email2 valid_email2
web-console web-console
webmock webmock
webpacker (~> 5.x) webpacker (~> 5.4.0)
webpush webpush
wisper (= 2.0.0) wisper (= 2.0.0)

View file

@ -148,6 +148,14 @@ class Messages::Facebook::MessageBuilder
} }
end 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 def contact_params
begin begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? 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 rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
raise 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 rescue StandardError => e
result = {} result = {}
Sentry.capture_exception(e) Sentry.capture_exception(e)
end end
{ 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 end
end end

View file

@ -41,19 +41,25 @@ class V2::ReportBuilder
user user
when :label when :label
label label
when :team
team
end end
end end
def inbox def inbox
@inbox ||= account.inboxes.where(id: params[:id]).first @inbox ||= account.inboxes.find(params[:id])
end end
def user def user
@user ||= account.users.where(id: params[:id]).first @user ||= account.users.find(params[:id])
end end
def label 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 end
def conversations_count def conversations_count

View file

@ -10,7 +10,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :check_authorization before_action :check_authorization
before_action :set_current_page, only: [:index, :active, :search] 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] before_action :set_include_contact_inboxes, only: [:index, :search]
def index def index
@ -30,10 +30,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end end
def import 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 ActiveRecord::Base.transaction do
import = Current.account.data_imports.create!(data_type: 'contacts') import = Current.account.data_imports.create!(data_type: 'contacts')
import.import_file.attach(params[:import_file]) import.import_file.attach(params[:import_file])
end end
head :ok head :ok
end end
@ -70,6 +73,18 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
}, status: :unprocessable_entity }, status: :unprocessable_entity
end 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 private
# TODO: Move this to a finder class # TODO: Move this to a finder class
@ -134,4 +149,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def fetch_contact def fetch_contact
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id]) @contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end end
def render_error(error, error_status)
render json: error, status: error_status
end
end end

View file

@ -69,6 +69,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def update_last_seen def update_last_seen
@conversation.agent_last_seen_at = DateTime.now.utc @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! @conversation.save!
end end
@ -112,6 +118,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def conversation_params def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {} additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {} status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases # 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_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id, contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes, additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until] snoozed_until: params[:snoozed_until]
}.merge(status) }.merge(status)
end end
@ -129,4 +137,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def conversation_finder def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params) @conversation_finder ||= ConversationFinder.new(current_user, params)
end end
def assignee?
@conversation.assignee_id? && current_user == @conversation.assignee
end
end end

View file

@ -100,6 +100,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end end
def update_channel_feature_flags def update_channel_feature_flags
return unless @inbox.web_widget?
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags 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] @inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]

View file

@ -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' render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
end 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 private
def check_authorization def check_authorization

View file

@ -3,10 +3,9 @@ class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
def index def index
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a @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) @accounts_count = number_with_delimiter(Account.count)
@users_count = number_with_delimiter(User.all.length) @users_count = number_with_delimiter(User.count)
@inboxes_count = number_with_delimiter(Inbox.all.length) @inboxes_count = number_with_delimiter(Inbox.count)
@conversations_count = number_with_delimiter(Conversation.all.length) @conversations_count = number_with_delimiter(Conversation.count)
@messages_count = number_with_delimiter(Message.all.length)
end end
end end

View file

@ -52,6 +52,14 @@ class ContactAPI extends ApiClient {
)}`; )}`;
return axios.get(requestURL); 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(); export default new ContactAPI();

View file

@ -59,6 +59,18 @@ describe('#ContactsAPI', () => {
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support' '/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' },
}
);
});
}); });
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -0,0 +1,3 @@
.margin-right-small {
margin-right: var(--space-small);
}

View file

@ -71,7 +71,8 @@
@include padding($space-large); @include padding($space-large);
} }
form { form,
.modal-content {
@include padding($space-large); @include padding($space-large);
align-self: center; align-self: center;

View file

@ -194,7 +194,7 @@ export default {
}); });
}, },
methods: { methods: {
handleKeyEvents(e) { getKeyboardListenerParams() {
const allConversations = this.$refs.activeConversation.querySelectorAll( const allConversations = this.$refs.activeConversation.querySelectorAll(
'div.conversations-list div.conversation' 'div.conversations-list div.conversation'
); );
@ -205,7 +205,19 @@ export default {
activeConversation activeConversation
); );
const lastConversationIndex = allConversations.length - 1; const lastConversationIndex = allConversations.length - 1;
return {
allConversations,
activeConversation,
activeConversationIndex,
lastConversationIndex,
};
},
handleKeyEvents(e) {
if (hasPressedAltAndJKey(e)) { if (hasPressedAltAndJKey(e)) {
const {
allConversations,
activeConversationIndex,
} = this.getKeyboardListenerParams();
if (activeConversationIndex === -1) { if (activeConversationIndex === -1) {
allConversations[0].click(); allConversations[0].click();
} }
@ -214,6 +226,11 @@ export default {
} }
} }
if (hasPressedAltAndKKey(e)) { if (hasPressedAltAndKKey(e)) {
const {
allConversations,
activeConversationIndex,
lastConversationIndex,
} = this.getKeyboardListenerParams();
if (activeConversationIndex === -1) { if (activeConversationIndex === -1) {
allConversations[lastConversationIndex].click(); allConversations[lastConversationIndex].click();
} else if (activeConversationIndex < lastConversationIndex) { } else if (activeConversationIndex < lastConversationIndex) {

View file

@ -176,7 +176,9 @@ export default {
'.conversations-list .conversation' '.conversations-list .conversation'
); );
if (hasPressedAltAndMKey(e)) { if (hasPressedAltAndMKey(e)) {
this.$refs.arrowDownButton.$el.click(); if (this.$refs.arrowDownButton) {
this.$refs.arrowDownButton.$el.click();
}
} }
if (hasPressedAltAndEKey(e)) { if (hasPressedAltAndEKey(e)) {
const activeConversation = document.querySelector( const activeConversation = document.querySelector(

View file

@ -95,10 +95,15 @@ import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel'; import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal'; import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
import { import {
hasPressedAltAndCKey,
hasPressedAltAndRKey,
hasPressedAltAndSKey,
hasPressedAltAndVKey,
hasPressedCommandAndForwardSlash, hasPressedCommandAndForwardSlash,
isEscape, isEscape,
} from 'shared/helpers/KeyboardHelpers'; } from 'shared/helpers/KeyboardHelpers';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import router from '../../routes';
export default { export default {
components: { components: {
@ -276,6 +281,27 @@ export default {
if (isEscape(e)) { if (isEscape(e)) {
this.closeKeyShortcutModal(); 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() { toggleSupportChatWindow() {
window.$chatwoot.toggle(); window.$chatwoot.toggle();

View file

@ -33,7 +33,7 @@
<a href="#" :class="computedChildClass(child)"> <a href="#" :class="computedChildClass(child)">
<div class="wrap"> <div class="wrap">
<i <i
v-if="computedInboxClass(child)" v-if="menuItem.key === 'inbox'"
class="inbox-icon" class="inbox-icon"
:class="computedInboxClass(child)" :class="computedInboxClass(child)"
/> />
@ -59,17 +59,10 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import router from '../../routes'; import router from '../../routes';
import {
hasPressedAltAndCKey,
hasPressedAltAndVKey,
hasPressedAltAndRKey,
hasPressedAltAndSKey,
} from 'shared/helpers/KeyboardHelpers';
import adminMixin from '../../mixins/isAdmin'; import adminMixin from '../../mixins/isAdmin';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { getInboxClassByType } from 'dashboard/helper/inbox'; import { getInboxClassByType } from 'dashboard/helper/inbox';
export default { export default {
mixins: [adminMixin, eventListenerMixins], mixins: [adminMixin],
props: { props: {
menuItem: { menuItem: {
type: Object, 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) { showItem(item) {
return this.isAdmin && item.newLink !== undefined; return this.isAdmin && item.newLink !== undefined;
}, },

View file

@ -22,19 +22,33 @@
src="~dashboard/assets/images/fb-badge.png" src="~dashboard/assets/images/fb-badge.png"
/> />
<img <img
v-if="badge === 'Channel::TwitterProfile'" v-if="badge === 'twitter-tweet'"
id="badge" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/twitter-badge.png" src="~dashboard/assets/images/twitter-badge.png"
/> />
<img <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" id="badge"
class="source-badge" class="source-badge"
:style="badgeStyle" :style="badgeStyle"
src="~dashboard/assets/images/channels/whatsapp.png" 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 <img
v-if="badge === 'Channel::Line'" v-if="badge === 'Channel::Line'"
id="badge" id="badge"

View file

@ -11,7 +11,7 @@
<Thumbnail <Thumbnail
v-if="!hideThumbnail" v-if="!hideThumbnail"
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
:badge="chatMetadata.channel" :badge="inboxBadge"
class="columns" class="columns"
:username="currentContact.name" :username="currentContact.name"
:status="currentContact.availability_status" :status="currentContact.availability_status"
@ -68,13 +68,14 @@ import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time'; import timeMixin from '../../../mixins/time';
import router from '../../../routes'; import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper'; import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
export default { export default {
components: { components: {
Thumbnail, Thumbnail,
}, },
mixins: [timeMixin, conversationMixin, messageFormatterMixin], mixins: [inboxMixin, timeMixin, conversationMixin, messageFormatterMixin],
props: { props: {
activeLabel: { activeLabel: {
type: String, type: String,
@ -167,14 +168,14 @@ export default {
return this.getPlainText(subject || this.lastMessageInChat.content); return this.getPlainText(subject || this.lastMessageInChat.content);
}, },
chatInbox() { inbox() {
const { inbox_id: inboxId } = this.chat; const { inbox_id: inboxId } = this.chat;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId); const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
return stateInbox; return stateInbox;
}, },
computedInboxClass() { computedInboxClass() {
const { phone_number: phoneNumber, channel_type: type } = this.chatInbox; const { phone_number: phoneNumber, channel_type: type } = this.inbox;
const classByType = getInboxClassByType(type, phoneNumber); const classByType = getInboxClassByType(type, phoneNumber);
return classByType; return classByType;
}, },
@ -187,11 +188,10 @@ export default {
); );
}, },
inboxName() { inboxName() {
const stateInbox = this.chatInbox; const stateInbox = this.inbox;
return stateInbox.name || ''; return stateInbox.name || '';
}, },
}, },
methods: { methods: {
cardClick(chat) { cardClick(chat) {
const { activeInbox } = this; const { activeInbox } = this;

View file

@ -4,7 +4,7 @@
<Thumbnail <Thumbnail
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
size="40px" size="40px"
:badge="chatMetadata.channel" :badge="inboxBadge"
:username="currentContact.name" :username="currentContact.name"
:status="currentContact.availability_status" :status="currentContact.availability_status"
/> />
@ -42,6 +42,7 @@ import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import agentMixin from '../../../mixins/agentMixin.js'; import agentMixin from '../../../mixins/agentMixin.js';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import inboxMixin from 'shared/mixins/inboxMixin';
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers'; import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
export default { export default {
@ -49,7 +50,7 @@ export default {
MoreActions, MoreActions,
Thumbnail, Thumbnail,
}, },
mixins: [agentMixin, eventListenerMixins], mixins: [inboxMixin, agentMixin, eventListenerMixins],
props: { props: {
chat: { chat: {
type: Object, type: Object,
@ -78,6 +79,12 @@ export default {
return this.chat.meta; return this.chat.meta;
}, },
inbox() {
const { inbox_id: inboxId } = this.chat;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
return stateInbox;
},
currentContact() { currentContact() {
return this.$store.getters['contacts/getContact']( return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id this.chat.meta.sender.id

View file

@ -76,7 +76,6 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import EmojiInput from 'shared/components/emoji/EmojiInput'; import EmojiInput from 'shared/components/emoji/EmojiInput';
import CannedResponse from './CannedResponse'; import CannedResponse from './CannedResponse';
@ -108,13 +107,7 @@ export default {
ReplyBottomPanel, ReplyBottomPanel,
WootMessageEditor, WootMessageEditor,
}, },
mixins: [ mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
clickaway,
inboxMixin,
uiSettingsMixin,
alertMixin,
eventListenerMixins,
],
props: { props: {
selectedTweet: { selectedTweet: {
type: [Object, String], 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: { methods: {
toggleUserMention(currentMentionState) { toggleUserMention(currentMentionState) {
this.hasUserMention = currentMentionState; this.hasUserMention = currentMentionState;
@ -345,7 +347,10 @@ export default {
await this.$store.dispatch('sendMessage', messagePayload); await this.$store.dispatch('sendMessage', messagePayload);
this.$emit('scrollToMessage'); this.$emit('scrollToMessage');
} catch (error) { } catch (error) {
// Error const errorMessage =
error?.response?.data?.error ||
this.$t('CONVERSATION.MESSAGE_ERROR');
this.showAlert(errorMessage);
} }
this.hideEmojiPicker(); this.hideEmojiPicker();
} }

View file

@ -19,6 +19,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'conversation.typing_off': this.onTypingOff, 'conversation.typing_off': this.onTypingOff,
'conversation.contact_changed': this.onConversationContactChange, 'conversation.contact_changed': this.onConversationContactChange,
'presence.update': this.onPresenceUpdate, 'presence.update': this.onPresenceUpdate,
'contact.deleted': this.onContactDelete,
}; };
} }
@ -115,6 +116,14 @@ class ActionCableConnector extends BaseActionCableConnector {
fetchConversationStats = () => { fetchConversationStats = () => {
bus.$emit('fetch_conversation_stats'); bus.$emit('fetch_conversation_stats');
}; };
onContactDelete = data => {
this.app.$store.dispatch(
'contacts/deleteContactThroughConversations',
data.id
);
this.fetchConversationStats();
};
} }
export default { export default {

View file

@ -22,7 +22,10 @@ export const getInboxClassByType = (type, phoneNumber) => {
case INBOX_TYPES.EMAIL: case INBOX_TYPES.EMAIL:
return 'ion-ios-email'; return 'ion-ios-email';
case INBOX_TYPES.TELEGRAM:
return 'ion-ios-navigate';
default: default:
return ''; return 'ion-ios-chatbubble';
} }
}; };

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Vyberte časové pásmo", "TIMEZONE_LABEL": "Vyberte časové pásmo",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -54,6 +54,35 @@
"TITLE": "Create new contact", "TITLE": "Create new contact",
"DESC": "Add basic information details about the 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": { "CONTACT_FORM": {
"FORM": { "FORM": {
"SUBMIT": "Submit", "SUBMIT": "Submit",
@ -239,4 +268,4 @@
"ERROR_MESSAGE": "Could not merge contcts, try again!" "ERROR_MESSAGE": "Could not merge contcts, try again!"
} }
} }
} }

View file

@ -84,6 +84,7 @@
"CHANGE_AGENT": "Conversation Assignee changed", "CHANGE_AGENT": "Conversation Assignee changed",
"CHANGE_TEAM": "Conversation team changed", "CHANGE_TEAM": "Conversation team changed",
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", "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:", "SENT_BY": "Sent by:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "Select Agent", "SELECT_AGENT": "Select Agent",

View file

@ -56,6 +56,11 @@
"CHANNEL_AVATAR": { "CHANNEL_AVATAR": {
"LABEL": "Channel Avatar" "LABEL": "Channel Avatar"
}, },
"CHANNEL_WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Enter your Webhook URL",
"ERROR": "Please enter a valid URL"
},
"CHANNEL_DOMAIN": { "CHANNEL_DOMAIN": {
"LABEL": "Website Domain", "LABEL": "Website Domain",
"PLACEHOLDER": "Enter your website domain (eg: acme.com)" "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" "ERROR_MESSAGE": "We were not able to authenticate Twilio credentials, please try again"
} }
}, },
"SMS": { "SMS": {
"TITLE": "SMS Channel via Twilio", "TITLE": "SMS Channel via Twilio",
"DESC": "Start supporting your customers via SMS with Twilio integration." "DESC": "Start supporting your customers via SMS with Twilio integration."
}, },
"WHATSAPP": { "WHATSAPP": {
"TITLE": "Whatsapp Channel via Twilio", "TITLE": "Whatsapp Channel via Twilio",
"DESC": "Start supporting your customers via Whatsapp with Twilio integration." "DESC": "Start supporting your customers via Whatsapp with Twilio integration."
}, },
@ -195,6 +200,10 @@
"SUBMIT_BUTTON": "Create LINE Channel", "SUBMIT_BUTTON": "Create LINE Channel",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the LINE channel" "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": { "TELEGRAM_CHANNEL": {
@ -350,7 +359,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -13,18 +13,18 @@
"STATUS_TABS": [ "STATUS_TABS": [
{ {
"NAME": "Apri", "NAME": "Apri",
"KEY": "contaaperture" "KEY": "openCount"
}, },
{ {
"NAME": "Risolti", "NAME": "Risolti",
"KEY": "Conteggio" "KEY": "allConvCount"
} }
], ],
"ASSIGNEE_TYPE_TABS": [ "ASSIGNEE_TYPE_TABS": [
{ {
"NAME": "Miniera", "NAME": "Miniera",
"KEY": "Io", "KEY": "Io",
"COUNT_KEY": "contaMinore" "COUNT_KEY": "mineCount"
}, },
{ {
"NAME": "Non assegnato", "NAME": "Non assegnato",
@ -40,11 +40,11 @@
"CHAT_STATUS_ITEMS": [ "CHAT_STATUS_ITEMS": [
{ {
"TEXT": "Apri", "TEXT": "Apri",
"VALUE": "Aperto" "VALUE": "open"
}, },
{ {
"TEXT": "Risolti", "TEXT": "Risolti",
"VALUE": "risolto" "VALUE": "resolved"
}, },
{ {
"TEXT": "Pending", "TEXT": "Pending",

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -350,7 +350,7 @@
"TIMEZONE_LABEL": "Select timezone", "TIMEZONE_LABEL": "Select timezone",
"UPDATE": "Update business hours settings", "UPDATE": "Update business hours settings",
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox", "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.", "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.", "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": { "DAY": {

View file

@ -3,7 +3,7 @@
<span class="close-button" @click="onClose"> <span class="close-button" @click="onClose">
<i class="ion-android-close close-icon" /> <i class="ion-android-close close-icon" />
</span> </span>
<contact-info show-new-message :contact="contact" /> <contact-info show-new-message :contact="contact" @panel-close="onClose" />
<accordion-item <accordion-item
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')" :title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')" :is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"

View file

@ -24,12 +24,12 @@ export default {
computed: { computed: {
savedLabels() { savedLabels() {
const result = this.$store.getters['contactLabels/getContactLabels']( const availableContactLabels = this.$store.getters[
this.contactId '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({ ...mapGetters({

View file

@ -7,6 +7,7 @@
this-selected-contact-id="" this-selected-contact-id=""
:on-input-search="onInputSearch" :on-input-search="onInputSearch"
:on-toggle-create="onToggleCreate" :on-toggle-create="onToggleCreate"
:on-toggle-import="onToggleImport"
:header-title="label" :header-title="label"
/> />
<contacts-table <contacts-table
@ -30,6 +31,9 @@
:on-close="closeContactInfoPanel" :on-close="closeContactInfoPanel"
/> />
<create-contact :show="showCreateModal" @cancel="onToggleCreate" /> <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> </div>
</template> </template>
@ -41,6 +45,7 @@ import ContactsTable from './ContactsTable';
import ContactInfoPanel from './ContactInfoPanel'; import ContactInfoPanel from './ContactInfoPanel';
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact'; import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
import TableFooter from 'dashboard/components/widgets/TableFooter'; import TableFooter from 'dashboard/components/widgets/TableFooter';
import ImportContacts from './ImportContacts.vue';
const DEFAULT_PAGE = 1; const DEFAULT_PAGE = 1;
@ -51,6 +56,7 @@ export default {
TableFooter, TableFooter,
ContactInfoPanel, ContactInfoPanel,
CreateContact, CreateContact,
ImportContacts,
}, },
props: { props: {
label: { type: String, default: '' }, label: { type: String, default: '' },
@ -59,6 +65,7 @@ export default {
return { return {
searchQuery: '', searchQuery: '',
showCreateModal: false, showCreateModal: false,
showImportModal: false,
selectedContactId: '', selectedContactId: '',
sortConfig: { name: 'asc' }, sortConfig: { name: 'asc' },
}; };
@ -168,6 +175,9 @@ export default {
onToggleCreate() { onToggleCreate() {
this.showCreateModal = !this.showCreateModal; this.showCreateModal = !this.showCreateModal;
}, },
onToggleImport() {
this.showImportModal = !this.showImportModal;
},
onSortChange(params) { onSortChange(params) {
this.sortConfig = params; this.sortConfig = params;
this.fetchContacts(this.meta.currentPage); this.fetchContacts(this.meta.currentPage);

View file

@ -29,11 +29,20 @@
<woot-button <woot-button
color-scheme="success" color-scheme="success"
icon="ion-android-add-circle" icon="ion-android-add-circle"
@click="onToggleCreate" class="margin-right-small"
data-testid="create-new-contact" data-testid="create-new-contact"
@click="onToggleCreate"
> >
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }} {{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
</woot-button> </woot-button>
<woot-button
color-scheme="info"
icon="ion-android-upload"
@click="onToggleImport"
>
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
</woot-button>
</div> </div>
</div> </div>
</header> </header>
@ -41,7 +50,6 @@
<script> <script>
export default { export default {
components: {},
props: { props: {
headerTitle: { headerTitle: {
type: String, type: String,
@ -63,10 +71,15 @@ export default {
type: Function, type: Function,
default: () => {}, default: () => {},
}, },
onToggleImport: {
type: Function,
default: () => {},
},
}, },
data() { data() {
return { return {
showCreateModal: false, showCreateModal: false,
showImportModal: false,
}; };
}, },
computed: { computed: {
@ -78,6 +91,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~dashboard/assets/scss/_utility-helpers.scss';
.page-title { .page-title {
margin: 0; margin: 0;
} }

View file

@ -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>

View file

@ -48,30 +48,59 @@
/> />
</div> </div>
</div> </div>
<woot-button <div v-if="!showNewMessage">
v-if="!showNewMessage" <div>
class="edit-contact" <woot-button
variant="link" class="edit-contact"
size="small" variant="link"
@click="toggleEditModal" size="small"
> @click="toggleEditModal"
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }} >
</woot-button> {{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
<div v-else class="contact-actions"> </woot-button>
<woot-button </div>
class="new-message" <div v-if="isAdmin">
size="small expanded" <woot-button
@click="toggleConversationModal" class="delete-contact"
> variant="link"
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }} size="small"
</woot-button> color-scheme="alert"
<woot-button @click="toggleDeleteModal"
variant="smooth" :disabled="uiFlags.isDeleting"
size="small expanded" >
@click="toggleEditModal" {{ $t('DELETE_CONTACT.BUTTON_LABEL') }}
> </woot-button>
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }} </div>
</woot-button> </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> </div>
<edit-contact <edit-contact
v-if="showEditModal" v-if="showEditModal"
@ -80,11 +109,24 @@
@cancel="toggleEditModal" @cancel="toggleEditModal"
/> />
<new-conversation <new-conversation
v-if="contact.id"
:show="showConversationModal" :show="showConversationModal"
:contact="contact" :contact="contact"
@cancel="toggleConversationModal" @cancel="toggleConversationModal"
/> />
</div> </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> </div>
</template> </template>
<script> <script>
@ -93,6 +135,9 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import SocialIcons from './SocialIcons'; import SocialIcons from './SocialIcons';
import EditContact from './EditContact'; import EditContact from './EditContact';
import NewConversation from './NewConversation'; import NewConversation from './NewConversation';
import alertMixin from 'shared/mixins/alertMixin';
import adminMixin from '../../../../mixins/isAdmin';
import { mapGetters } from 'vuex';
export default { export default {
components: { components: {
@ -102,6 +147,7 @@ export default {
SocialIcons, SocialIcons,
NewConversation, NewConversation,
}, },
mixins: [alertMixin, adminMixin],
props: { props: {
contact: { contact: {
type: Object, type: Object,
@ -120,9 +166,11 @@ export default {
return { return {
showEditModal: false, showEditModal: false,
showConversationModal: false, showConversationModal: false,
showDeleteModal: false,
}; };
}, },
computed: { computed: {
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
additionalAttributes() { additionalAttributes() {
return this.contact.additional_attributes || {}; return this.contact.additional_attributes || {};
}, },
@ -134,6 +182,23 @@ export default {
return { twitter: twitterScreenName, ...(socialProfiles || {}) }; 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: { methods: {
toggleEditModal() { toggleEditModal() {
@ -142,6 +207,31 @@ export default {
toggleConversationModal() { toggleConversationModal() {
this.showConversationModal = !this.showConversationModal; 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> </script>
@ -179,17 +269,32 @@ export default {
.contact-actions { .contact-actions {
margin-top: var(--space-small); margin-top: var(--space-small);
} }
.button.edit-contact {
.edit-contact {
margin-left: var(--space-medium); margin-left: var(--space-medium);
} }
.button.new-message { .delete-contact {
margin-right: var(--space-small); margin-left: var(--space-medium);
} }
.contact-actions { .contact-actions {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; 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> </style>

View file

@ -237,7 +237,7 @@ export default {
if (this.isOngoingType) { if (this.isOngoingType) {
return this.$store.getters['inboxes/getWebsiteInboxes']; return this.$store.getters['inboxes/getWebsiteInboxes'];
} }
return this.$store.getters['inboxes/getTwilioInboxes']; return this.$store.getters['inboxes/getTwilioSMSInboxes'];
}, },
sendersAndBotList() { sendersAndBotList() {
return [ return [

View file

@ -161,7 +161,7 @@ export default {
if (this.isOngoingType) { if (this.isOngoingType) {
return this.$store.getters['inboxes/getWebsiteInboxes']; return this.$store.getters['inboxes/getWebsiteInboxes'];
} }
return this.$store.getters['inboxes/getTwilioInboxes']; return this.$store.getters['inboxes/getTwilioSMSInboxes'];
}, },
pageTitle() { pageTitle() {
return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${ return `${this.$t('CAMPAIGN.EDIT.TITLE')} - ${

View file

@ -88,7 +88,7 @@ export default {
const selectedAgents = this.selectedAgents.map(x => x.id); const selectedAgents = this.selectedAgents.map(x => x.id);
try { try {
await InboxMembersAPI.create({ inboxId, agentList: selectedAgents }); await InboxMembersAPI.update({ inboxId, agentList: selectedAgents });
router.replace({ router.replace({
name: 'settings_inbox_finish', name: 'settings_inbox_finish',
params: { params: {

View file

@ -17,7 +17,7 @@
<woot-code <woot-code
v-if="isATwilioInbox" v-if="isATwilioInbox"
lang="html" lang="html"
:script="currentInbox.webhook_url" :script="currentInbox.callback_webhook_url"
> >
</woot-code> </woot-code>
</div> </div>
@ -25,7 +25,7 @@
<woot-code <woot-code
v-if="isALineInbox" v-if="isALineInbox"
lang="html" lang="html"
:script="currentInbox.webhook_url" :script="currentInbox.callback_webhook_url"
> >
</woot-code> </woot-code>
</div> </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) { if (this.isAEmailInbox) {
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE'); return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
} }

View file

@ -51,6 +51,9 @@
<span v-if="item.channel_type === 'Channel::Telegram'"> <span v-if="item.channel_type === 'Channel::Telegram'">
Telegram Telegram
</span> </span>
<span v-if="item.channel_type === 'Channel::Line'">
Line
</span>
<span v-if="item.channel_type === 'Channel::Api'"> <span v-if="item.channel_type === 'Channel::Api'">
{{ globalConfig.apiChannelName || 'API' }} {{ globalConfig.apiChannelName || 'API' }}
</span> </span>

View file

@ -22,7 +22,7 @@
<woot-avatar-uploader <woot-avatar-uploader
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AVATAR.LABEL')" :label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AVATAR.LABEL')"
:src="avatarUrl" :src="avatarUrl"
deleteAvatar delete-avatar
@change="handleImageUpload" @change="handleImageUpload"
@onAvatarDelete="handleAvatarDelete" @onAvatarDelete="handleAvatarDelete"
/> />
@ -32,6 +32,24 @@
:label="inboxNameLabel" :label="inboxNameLabel"
:placeholder="inboxNamePlaceHolder" :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 <woot-input
v-if="isAWebWidgetInbox" v-if="isAWebWidgetInbox"
v-model.trim="channelWebsiteUrl" v-model.trim="channelWebsiteUrl"
@ -212,6 +230,16 @@
</div> </div>
<woot-submit-button <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')" :button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:loading="uiFlags.isUpdatingInbox" :loading="uiFlags.isUpdatingInbox"
@click="updateInbox" @click="updateInbox"
@ -259,7 +287,21 @@
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')" :title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')" :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> </settings-section>
</div> </div>
<div v-else-if="isAWebWidgetInbox"> <div v-else-if="isAWebWidgetInbox">
@ -310,6 +352,8 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { createMessengerScript } from 'dashboard/helper/scriptGenerator'; 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 configMixin from 'shared/mixins/configMixin';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner'; import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner';
@ -343,6 +387,7 @@ export default {
csatSurveyEnabled: false, csatSurveyEnabled: false,
selectedInboxName: '', selectedInboxName: '',
channelWebsiteUrl: '', channelWebsiteUrl: '',
webhookUrl: '',
channelWelcomeTitle: '', channelWelcomeTitle: '',
channelWelcomeTagline: '', channelWelcomeTagline: '',
selectedFeatureFlags: [], selectedFeatureFlags: [],
@ -378,6 +423,10 @@ export default {
key: 'collaborators', key: 'collaborators',
name: this.$t('INBOX_MGMT.TABS.COLLABORATORS'), name: this.$t('INBOX_MGMT.TABS.COLLABORATORS'),
}, },
{
key: 'businesshours',
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
},
]; ];
if (this.isAWebWidgetInbox) { if (this.isAWebWidgetInbox) {
@ -387,10 +436,6 @@ export default {
key: 'preChatForm', key: 'preChatForm',
name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'), name: this.$t('INBOX_MGMT.TABS.PRE_CHAT_FORM'),
}, },
{
key: 'businesshours',
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
},
{ {
key: 'configuration', key: 'configuration',
name: this.$t('INBOX_MGMT.TABS.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 [ return [
...visibleToAllChannelTabs, ...visibleToAllChannelTabs,
{ {
@ -484,6 +534,7 @@ export default {
this.fetchAttachedAgents(); this.fetchAttachedAgents();
this.avatarUrl = this.inbox.avatar_url; this.avatarUrl = this.inbox.avatar_url;
this.selectedInboxName = this.inbox.name; this.selectedInboxName = this.inbox.name;
this.webhookUrl = this.inbox.webhook_url;
this.greetingEnabled = this.inbox.greeting_enabled || false; this.greetingEnabled = this.inbox.greeting_enabled || false;
this.greetingMessage = this.inbox.greeting_message || ''; this.greetingMessage = this.inbox.greeting_message || '';
this.autoAssignment = this.inbox.enable_auto_assignment; this.autoAssignment = this.inbox.enable_auto_assignment;
@ -536,6 +587,7 @@ export default {
channel: { channel: {
widget_color: this.inbox.widget_color, widget_color: this.inbox.widget_color,
website_url: this.channelWebsiteUrl, website_url: this.channelWebsiteUrl,
webhook_url: this.webhookUrl,
welcome_title: this.channelWelcomeTitle || '', welcome_title: this.channelWelcomeTitle || '',
welcome_tagline: this.channelWelcomeTagline || '', welcome_tagline: this.channelWelcomeTagline || '',
selectedFeatureFlags: this.selectedFeatureFlags, selectedFeatureFlags: this.selectedFeatureFlags,
@ -574,6 +626,10 @@ export default {
}, },
}, },
validations: { validations: {
webhookUrl: {
required,
shouldBeUrl,
},
selectedAgents: { selectedAgents: {
isEmpty() { isEmpty() {
return !!this.selectedAgents.length; return !!this.selectedAgents.length;

View file

@ -82,6 +82,9 @@ export const mutations = {
const conversations = $state.records[id] || []; const conversations = $state.records[id] || [];
Vue.set($state.records, id, [...conversations, data]); Vue.set($state.records, id, [...conversations, data]);
}, },
[types.default.DELETE_CONTACT_CONVERSATION]: ($state, id) => {
Vue.delete($state.records, id);
},
}; };
export default { export default {

View file

@ -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) => { fetchContactableInbox: async ({ commit }, id) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true }); commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
@ -110,4 +136,12 @@ export const actions = {
setContact({ commit }, data) { setContact({ commit }, data) {
commit(types.SET_CONTACT_ITEM, 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,
});
},
}; };

View file

@ -13,6 +13,7 @@ const state = {
isFetchingItem: false, isFetchingItem: false,
isFetchingInboxes: false, isFetchingInboxes: false,
isUpdating: false, isUpdating: false,
isDeleting: false,
}, },
sortOrder: [], sortOrder: [],
}; };

View file

@ -46,6 +46,12 @@ export const mutations = {
Vue.set($state.records, data.id, data); 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) => { [types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
Object.values($state.records).forEach(element => { Object.values($state.records).forEach(element => {
const availabilityStatus = data[element.id]; const availabilityStatus = data[element.id];

View file

@ -155,6 +155,7 @@ const actions = {
}, },
sendMessage: async ({ commit }, data) => { sendMessage: async ({ commit }, data) => {
// eslint-disable-next-line no-useless-catch
try { try {
const pendingMessage = createPendingMessage(data); const pendingMessage = createPendingMessage(data);
commit(types.default.ADD_MESSAGE, pendingMessage); commit(types.default.ADD_MESSAGE, pendingMessage);
@ -164,7 +165,7 @@ const actions = {
status: MESSAGE_STATUS.SENT, status: MESSAGE_STATUS.SENT,
}); });
} catch (error) { } catch (error) {
// Handle error throw error;
} }
}, },

View file

@ -177,6 +177,13 @@ export const mutations = {
Vue.set(chat, 'can_reply', canReply); 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 { export default {

View file

@ -73,6 +73,11 @@ export const getters = {
item => item.channel_type === INBOX_TYPES.TWILIO 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 = { export const actions = {

View file

@ -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', () => { describe('#setContact', () => {
it('returns correct mutations', () => { it('returns correct mutations', () => {
const data = { id: 1, name: 'john doe', availability_status: 'online' }; 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]]); 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 },
],
]);
});
});
}); });

View file

@ -49,10 +49,10 @@ export default [
name: 'Test Widget 5', name: 'Test Widget 5',
channel_type: 'Channel::TwilioSms', channel_type: 'Channel::TwilioSms',
avatar_url: null, avatar_url: null,
medium: 'sms',
page_id: null, page_id: null,
widget_color: '#68BC00', widget_color: '#68BC00',
website_token: 'randomid125', website_token: 'randomid125',
enable_auto_assignment: true, enable_auto_assignment: true,
}, },
]; ];

View file

@ -19,6 +19,11 @@ describe('#getters', () => {
expect(getters.getTwilioInboxes(state).length).toEqual(1); expect(getters.getTwilioInboxes(state).length).toEqual(1);
}); });
it('getTwilioSMSInboxes', () => {
const state = { records: inboxList };
expect(getters.getTwilioSMSInboxes(state).length).toEqual(1);
});
it('getInbox', () => { it('getInbox', () => {
const state = { const state = {
records: inboxList, records: inboxList,

View file

@ -18,6 +18,7 @@ export default {
CHANGE_CHAT_STATUS_FILTER: 'CHANGE_CHAT_STATUS_FILTER', CHANGE_CHAT_STATUS_FILTER: 'CHANGE_CHAT_STATUS_FILTER',
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE', UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT', UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW', SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW', CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
@ -101,6 +102,7 @@ export default {
SET_CONTACTS: 'SET_CONTACTS', SET_CONTACTS: 'SET_CONTACTS',
CLEAR_CONTACTS: 'CLEAR_CONTACTS', CLEAR_CONTACTS: 'CLEAR_CONTACTS',
EDIT_CONTACT: 'EDIT_CONTACT', EDIT_CONTACT: 'EDIT_CONTACT',
DELETE_CONTACT: 'DELETE_CONTACT',
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE', UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
// Notifications // Notifications
@ -119,6 +121,7 @@ export default {
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG', SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS', SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION', ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION',
DELETE_CONTACT_CONVERSATION: 'DELETE_CONTACT_CONVERSATION',
// Contact Label // Contact Label
SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG', SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG',

View file

@ -40,8 +40,8 @@ const runSDK = ({ baseUrl, websiteToken }) => {
launcherTitle: chatwootSettings.launcherTitle || '', launcherTitle: chatwootSettings.launcherTitle || '',
showPopoutButton: chatwootSettings.showPopoutButton || false, showPopoutButton: chatwootSettings.showPopoutButton || false,
toggle() { toggle(state) {
IFrameHelper.events.toggleBubble(); IFrameHelper.events.toggleBubble(state);
}, },
setUser(identifier, user) { setUser(identifier, user) {

View file

@ -148,8 +148,15 @@ export const IFrameHelper = {
setBubbleText(window.$chatwoot.launcherTitle || message.label); setBubbleText(window.$chatwoot.launcherTitle || message.label);
}, },
toggleBubble: () => { toggleBubble: state => {
onBubbleClick(); let bubbleState = {};
if (state === 'open') {
bubbleState.toggleValue = true;
} else if (state === 'close') {
bubbleState.toggleValue = false;
}
onBubbleClick(bubbleState);
}, },
onBubbleToggle: isOpen => { onBubbleToggle: isOpen => {

View file

@ -99,6 +99,7 @@ export const SDK_CSS = `.woot-widget-holder {
.woot--close::before, .woot--close::after { .woot--close::before, .woot--close::after {
background-color: #fff; background-color: #fff;
content: ' '; content: ' ';
display: inline;
height: 24px; height: 24px;
left: 32px; left: 32px;
position: absolute; position: absolute;
@ -149,7 +150,7 @@ export const SDK_CSS = `.woot-widget-holder {
max-height: 100vh; max-height: 100vh;
padding: 0 8px; padding: 0 8px;
} }
.woot-widget-holder.has-unread-view iframe { .woot-widget-holder.has-unread-view iframe {
min-height: unset !important; min-height: unset !important;
} }
@ -157,7 +158,7 @@ export const SDK_CSS = `.woot-widget-holder {
.woot-widget-holder.has-unread-view.woot-elements--left { .woot-widget-holder.has-unread-view.woot-elements--left {
left: 0; left: 0;
} }
.woot-widget-bubble.woot--close { .woot-widget-bubble.woot--close {
bottom: 60px; bottom: 60px;
opacity: 0; opacity: 0;

View file

@ -41,10 +41,11 @@ export default {
@import '~widget/assets/scss/variables.scss'; @import '~widget/assets/scss/variables.scss';
.option { .option {
border: 1px solid $color-woot;
border-radius: $space-jumbo; border-radius: $space-jumbo;
border: 1px solid $color-woot;
float: left; float: left;
margin: $space-smaller; margin: $space-smaller;
max-width: 100%;
.option-button { .option-button {
background: transparent; background: transparent;
@ -52,7 +53,11 @@ export default {
border: 0; border: 0;
color: $color-woot; color: $color-woot;
cursor: pointer; cursor: pointer;
height: auto;
line-height: 1.5;
min-height: $space-two * 2;
text-align: left; text-align: left;
white-space: normal;
span { span {
display: inline-block; display: inline-block;

View file

@ -16,7 +16,6 @@
:src="selectedItem.thumbnail" :src="selectedItem.thumbnail"
size="24px" size="24px"
:status="selectedItem.availability_status" :status="selectedItem.availability_status"
:badge="selectedItem.channel"
:username="selectedItem.name" :username="selectedItem.name"
/> />
<div class="selector-name-wrap"> <div class="selector-name-wrap">

View file

@ -1,2 +1,4 @@
export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/); export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/);
export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === ''; export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === '';
export const shouldBeUrl = (value = '') =>
value ? value.startsWith('http') : true;

View file

@ -0,0 +1,7 @@
import { shouldBeUrl } from '../Validators';
describe('#shouldBeUrl', () => {
it('should return correct url', () => {
expect(shouldBeUrl('http')).toEqual(true);
});
});

View file

@ -1,8 +1,28 @@
import { isEscape } from '../helpers/KeyboardHelpers';
export default { export default {
mounted() { mounted() {
document.addEventListener('keydown', this.handleKeyEvents); document.addEventListener('keydown', this.onKeyDownHandler);
}, },
destroyed() { beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyEvents); 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);
},
}, },
}; };

View file

@ -5,6 +5,8 @@ export const INBOX_TYPES = {
TWILIO: 'Channel::TwilioSms', TWILIO: 'Channel::TwilioSms',
API: 'Channel::Api', API: 'Channel::Api',
EMAIL: 'Channel::Email', EMAIL: 'Channel::Email',
TELEGRAM: 'Channel::Telegram',
LINE: 'Channel::Line',
}; };
export default { export default {
@ -27,16 +29,41 @@ export default {
isATwilioChannel() { isATwilioChannel() {
return this.channelType === INBOX_TYPES.TWILIO; return this.channelType === INBOX_TYPES.TWILIO;
}, },
isALineChannel() {
return this.channelType === INBOX_TYPES.LINE;
},
isAnEmailChannel() { isAnEmailChannel() {
return this.channelType === INBOX_TYPES.EMAIL; return this.channelType === INBOX_TYPES.EMAIL;
}, },
isATwilioSMSChannel() { isATwilioSMSChannel() {
const { phone_number: phoneNumber = '' } = this.inbox; const { medium: medium = '' } = this.inbox;
return this.isATwilioChannel && !phoneNumber.startsWith('whatsapp'); return this.isATwilioChannel && medium === 'sms';
}, },
isATwilioWhatsappChannel() { isATwilioWhatsappChannel() {
const { phone_number: phoneNumber = '' } = this.inbox; const { medium: medium = '' } = this.inbox;
return this.isATwilioChannel && phoneNumber.startsWith('whatsapp'); 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;
}, },
}, },
}; };

View file

@ -70,7 +70,23 @@ describe('inboxMixin', () => {
return { return {
inbox: { inbox: {
channel_type: 'Channel::TwilioSms', 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); 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 = { const Component = {
render() {}, render() {},
mixins: [inboxMixin], mixins: [inboxMixin],
@ -88,7 +104,7 @@ describe('inboxMixin', () => {
return { return {
inbox: { inbox: {
channel_type: 'Channel::TwilioSms', channel_type: 'Channel::TwilioSms',
phone_number: 'whatsapp:+91944444444', medium: 'whatsapp',
}, },
}; };
}, },
@ -111,4 +127,79 @@ describe('inboxMixin', () => {
const wrapper = shallowMount(Component); const wrapper = shallowMount(Component);
expect(wrapper.vm.isAnEmailChannel).toBe(true); 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');
});
}); });

View file

@ -27,6 +27,7 @@ class ContactIpLookupJob < ApplicationJob
geocoder_result = Geocoder.search(ip).first geocoder_result = Geocoder.search(ip).first
return unless geocoder_result return unless geocoder_result
contact.additional_attributes ||= {}
contact.additional_attributes['city'] = geocoder_result.city contact.additional_attributes['city'] = geocoder_result.city
contact.additional_attributes['country'] = geocoder_result.country contact.additional_attributes['country'] = geocoder_result.country
contact.additional_attributes['country_code'] = geocoder_result.country_code contact.additional_attributes['country_code'] = geocoder_result.country_code
@ -34,7 +35,7 @@ class ContactIpLookupJob < ApplicationJob
end end
def get_contact_ip(contact) 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 end
def ensure_look_up_db def ensure_look_up_db

View 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

View file

@ -111,6 +111,13 @@ class ActionCableListener < BaseListener
broadcast(account, tokens, CONTACT_MERGED, contact.push_event_data) broadcast(account, tokens, CONTACT_MERGED, contact.push_event_data)
end 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 private
def typing_event_listener_tokens(account, conversation, user) def typing_event_listener_tokens(account, conversation, user)

View file

@ -22,7 +22,7 @@ class ApplicationMailbox < ActionMailbox::Base
proc do |inbound_mail_obj| proc do |inbound_mail_obj|
is_a_support_email = false is_a_support_email = false
inbound_mail_obj.mail.to&.each do |email| 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? if channel.present?
is_a_support_email = true is_a_support_email = true
break break

View file

@ -21,7 +21,7 @@ class SupportMailbox < ApplicationMailbox
def find_channel def find_channel
mail.to.each do |email| 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? break if @channel.present?
end end
raise 'Email channel/inbox not found' if @channel.nil? raise 'Email channel/inbox not found' if @channel.nil?
@ -82,6 +82,6 @@ class SupportMailbox < ApplicationMailbox
end end
def identify_contact_name def identify_contact_name
processed_mail.from.first.split('@').first processed_mail.sender_name || processed_mail.from.first.split('@').first
end end
end end

View file

@ -25,6 +25,7 @@ class Attachment < ApplicationRecord
enum file_type: [:image, :audio, :video, :file, :location, :fallback] enum file_type: [:image, :audio, :video, :file, :location, :fallback]
def push_event_data def push_event_data
return unless file_type
return base_data.merge(location_metadata) if file_type.to_sym == :location return base_data.merge(location_metadata) if file_type.to_sym == :location
return base_data.merge(fallback_data) if file_type.to_sym == :fallback return base_data.merge(fallback_data) if file_type.to_sym == :fallback

View file

@ -48,6 +48,7 @@ class Contact < ApplicationRecord
before_validation :prepare_email_attribute before_validation :prepare_email_attribute
after_create_commit :dispatch_create_event, :ip_lookup after_create_commit :dispatch_create_event, :ip_lookup
after_update_commit :dispatch_update_event after_update_commit :dispatch_update_event
after_destroy_commit :dispatch_destroy_event
def get_source_id(inbox_id) def get_source_id(inbox_id)
contact_inboxes.find_by!(inbox_id: inbox_id).source_id contact_inboxes.find_by!(inbox_id: inbox_id).source_id
@ -73,7 +74,8 @@ class Contact < ApplicationRecord
id: id, id: id,
name: name, name: name,
avatar: avatar_url, avatar: avatar_url,
type: 'contact' type: 'contact',
account: account.webhook_data
} }
end end
@ -98,4 +100,8 @@ class Contact < ApplicationRecord
def dispatch_update_event def dispatch_update_event
Rails.configuration.dispatcher.dispatch(CONTACT_UPDATED, Time.zone.now, contact: self) Rails.configuration.dispatcher.dispatch(CONTACT_UPDATED, Time.zone.now, contact: self)
end end
def dispatch_destroy_event
Rails.configuration.dispatcher.dispatch(CONTACT_DELETED, Time.zone.now, contact: self)
end
end end

View file

@ -5,7 +5,9 @@
# id :integer not null, primary key # id :integer not null, primary key
# additional_attributes :jsonb # additional_attributes :jsonb
# agent_last_seen_at :datetime # agent_last_seen_at :datetime
# assignee_last_seen_at :datetime
# contact_last_seen_at :datetime # contact_last_seen_at :datetime
# custom_attributes :jsonb
# identifier :string # identifier :string
# last_activity_at :datetime not null # last_activity_at :datetime not null
# snoozed_until :datetime # snoozed_until :datetime

View file

@ -31,6 +31,7 @@ class Inbox < ApplicationRecord
include Avatarable include Avatarable
include OutOfOffisable include OutOfOffisable
validates :name, presence: true
validates :account_id, presence: true validates :account_id, presence: true
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers } validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }
@ -82,6 +83,10 @@ class Inbox < ApplicationRecord
channel_type == 'Channel::Email' channel_type == 'Channel::Email'
end end
def twilio?
channel_type == 'Channel::TwilioSms'
end
def inbox_type def inbox_type
channel.name channel.name
end end
@ -93,9 +98,9 @@ class Inbox < ApplicationRecord
} }
end end
def webhook_url def callback_webhook_url
case channel_type case channel_type
when 'Channel::TwilioSMS' when 'Channel::TwilioSms'
"#{ENV['FRONTEND_URL']}/twilio/callback" "#{ENV['FRONTEND_URL']}/twilio/callback"
when 'Channel::Line' when 'Channel::Line'
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}" "#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"

View file

@ -25,6 +25,8 @@ class Label < ApplicationRecord
format: { with: UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE }, format: { with: UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE },
uniqueness: { scope: :account_id } uniqueness: { scope: :account_id }
after_update_commit :update_associated_models
before_validation do before_validation do
self.title = title.downcase if attribute_present?('title') self.title = title.downcase if attribute_present?('title')
end end
@ -40,4 +42,12 @@ class Label < ApplicationRecord
def events def events
account.events.where(conversation_id: conversations.pluck(:id)) account.events.where(conversation_id: conversations.pluck(:id))
end end
private
def update_associated_models
return unless title_previously_changed?
Labels::UpdateJob.perform_later(title, title_previously_was, account_id)
end
end end

View file

@ -40,4 +40,12 @@ class Team < ApplicationRecord
def remove_member(user_id) def remove_member(user_id)
team_members.find_by(user_id: user_id)&.destroy team_members.find_by(user_id: user_id)&.destroy
end end
def messages
account.messages.where(conversation_id: conversations.pluck(:id))
end
def events
account.events.where(conversation_id: conversations.pluck(:id))
end
end end

View file

@ -43,10 +43,10 @@ class WorkingHour < ApplicationRecord
def open_at?(time) def open_at?(time)
return false if closed_all_day? return false if closed_all_day?
time.hour >= open_hour && open_time = Time.zone.now.in_time_zone(inbox.timezone).change({ hour: open_hour, min: open_minutes })
time.min >= open_minutes && close_time = Time.zone.now.in_time_zone(inbox.timezone).change({ hour: close_hour, min: close_minutes })
time.hour <= close_hour &&
time.min <= close_minutes time.between?(open_time, close_time)
end end
def open_now? def open_now?

View file

@ -30,4 +30,8 @@ class ContactPolicy < ApplicationPolicy
def create? def create?
true true
end end
def destroy?
@account_user.administrator?
end
end end

View file

@ -82,6 +82,10 @@ class MailPresenter < SimpleDelegator
@mail.from.map(&:downcase) @mail.from.map(&:downcase)
end end
def sender_name
Mail::Address.new(@mail[:from].value).name
end
def original_sender def original_sender
@mail['X-Original-Sender'].try(:value) || from.first @mail['X-Original-Sender'].try(:value) || from.first
end end

View 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

View file

@ -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 class Line::IncomingMessageService
include ::FileTypeHelper include ::FileTypeHelper
pattr_initialize [:inbox!, :params!] pattr_initialize [:inbox!, :params!]
def perform def perform
# probably test events
return if params[:events].blank?
line_contact_info line_contact_info
return if line_contact_info['userId'].blank?
set_contact set_contact
set_conversation set_conversation
# TODO: iterate over the events and handle the attachments in future # TODO: iterate over the events and handle the attachments in future

View file

@ -14,14 +14,18 @@ class MessageTemplates::HookExecutionService
delegate :contact, to: :conversation delegate :contact, to: :conversation
def trigger_templates 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::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::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? ::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform if should_send_csat_survey?
end end
def should_send_out_of_office_message? 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? inbox.out_of_office? && conversation.messages.today.template.empty? && inbox.out_of_office_message.present?
end end

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