Merge branch 'develop' of https://github.com/chatwoot/chatwoot into spike/multiple-conv-widget
This commit is contained in:
commit
430c0b1569
31 changed files with 252 additions and 94 deletions
|
@ -161,3 +161,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||
|
||||
# If you want to use official mobile app,
|
||||
# the notifications would be relayed via a Chatwoot server
|
||||
ENABLE_PUSH_RELAY_SERVER=true
|
||||
|
|
4
app.json
4
app.json
|
@ -28,6 +28,10 @@
|
|||
"FRONTEND_URL": {
|
||||
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
|
||||
"value": "https://CHANGE.herokuapp.com"
|
||||
},
|
||||
"INSTALLATION_ENV": {
|
||||
"description": "Installation method used for Chatwoot.",
|
||||
"value": "heroku"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
|
|
@ -3,8 +3,9 @@ class ContactIdentifyAction
|
|||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||
merge_if_existing_identified_contact
|
||||
merge_if_existing_email_contact
|
||||
merge_if_existing_phone_number_contact
|
||||
update_contact
|
||||
end
|
||||
@contact
|
||||
|
@ -16,6 +17,18 @@ class ContactIdentifyAction
|
|||
@account ||= @contact.account
|
||||
end
|
||||
|
||||
def merge_if_existing_identified_contact
|
||||
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
|
||||
end
|
||||
|
||||
def merge_if_existing_email_contact
|
||||
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
|
||||
end
|
||||
|
||||
def merge_if_existing_phone_number_contact
|
||||
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact)
|
||||
end
|
||||
|
||||
def existing_identified_contact
|
||||
return if params[:identifier].blank?
|
||||
|
||||
|
@ -28,6 +41,12 @@ class ContactIdentifyAction
|
|||
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
|
||||
end
|
||||
|
||||
def existing_phone_number_contact
|
||||
return if params[:phone_number].blank?
|
||||
|
||||
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number])
|
||||
end
|
||||
|
||||
def merge_contacts?(existing_contact, _contact)
|
||||
existing_contact && existing_contact.id != @contact.id
|
||||
end
|
||||
|
@ -36,7 +55,9 @@ class ContactIdentifyAction
|
|||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||
# blank identifier or email will throw unique index error
|
||||
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
|
||||
@contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes }))
|
||||
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
|
||||
v.blank?
|
||||
end.merge({ custom_attributes: custom_attributes }))
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||
|
||||
contacts = resolved_contacts.where(
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search',
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
||||
search: "%#{params[:q]}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
|
@ -108,7 +108,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
|
||||
end
|
||||
|
||||
def contact_custom_attributes
|
||||
|
|
|
@ -29,6 +29,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
|
||||
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,22 @@
|
|||
font-weight: var(--font-weight-light);
|
||||
margin-top: var(--space-large);
|
||||
}
|
||||
|
||||
.update-subscription--checkbox {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
line-height: 1.5;
|
||||
margin-right: var(--space-one);
|
||||
margin-top: var(--space-smaller);
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
|
@ -20,17 +36,4 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.update-subscription--checkbox {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
line-height: 1.5;
|
||||
margin-right: var(--space-one);
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
|||
methods: {
|
||||
openLink() {
|
||||
const win = window.open(this.url, '_blank', 'noopener');
|
||||
win.focus();
|
||||
if (win) win.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -45,9 +45,20 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
valueWithLink(attribute) {
|
||||
const messageFormatter = new MessageFormatter(attribute);
|
||||
const parsedAttribute = this.parseAttributeToString(attribute);
|
||||
const messageFormatter = new MessageFormatter(parsedAttribute);
|
||||
return messageFormatter.formattedMessage;
|
||||
},
|
||||
parseAttributeToString(attribute) {
|
||||
switch (typeof attribute) {
|
||||
case 'string':
|
||||
return attribute;
|
||||
case 'object':
|
||||
return JSON.stringify(attribute);
|
||||
default:
|
||||
return `${attribute}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -97,6 +97,15 @@ export const IFrameHelper = {
|
|||
}
|
||||
});
|
||||
},
|
||||
|
||||
setFrameHeightToFitContent: (extraHeight, isFixedHeight) => {
|
||||
const iframe = IFrameHelper.getAppFrame();
|
||||
const updatedIframeHeight = isFixedHeight ? `${extraHeight}px` : '100%';
|
||||
|
||||
if (iframe)
|
||||
iframe.setAttribute('style', `height: ${updatedIframeHeight} !important`);
|
||||
},
|
||||
|
||||
events: {
|
||||
loaded: message => {
|
||||
Cookies.set('cw_conversation', message.config.authToken, {
|
||||
|
@ -169,6 +178,13 @@ export const IFrameHelper = {
|
|||
}
|
||||
},
|
||||
|
||||
updateIframeHeight: message => {
|
||||
const { extraHeight = 0, isFixedHeight } = message;
|
||||
if (!extraHeight) return;
|
||||
|
||||
IFrameHelper.setFrameHeightToFitContent(extraHeight, isFixedHeight);
|
||||
},
|
||||
|
||||
resetUnreadMode: () => {
|
||||
IFrameHelper.sendMessage('unset-unread-view');
|
||||
IFrameHelper.events.removeUnreadClass();
|
||||
|
@ -178,22 +194,6 @@ export const IFrameHelper = {
|
|||
const holderEl = document.querySelector('.woot-widget-holder');
|
||||
removeClass(holderEl, 'has-unread-view');
|
||||
},
|
||||
|
||||
updateIframeHeight: message => {
|
||||
setTimeout(() => {
|
||||
const iframe = IFrameHelper.getAppFrame();
|
||||
const scrollableMessageHeight =
|
||||
iframe.contentWindow.document.querySelector('.unread-messages')
|
||||
.scrollHeight + 40;
|
||||
const updatedIframeHeight = message.isFixedHeight
|
||||
? `${scrollableMessageHeight}px`
|
||||
: '100%';
|
||||
iframe.setAttribute(
|
||||
'style',
|
||||
`height: ${updatedIframeHeight} !important`
|
||||
);
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
pushEvent: eventName => {
|
||||
IFrameHelper.sendMessage('push-event', { eventName });
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
export const SDK_CSS = `.woot-widget-holder {
|
||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important;
|
||||
opacity: 1;
|
||||
will-change: transform, opacity;
|
||||
transform: translateY(0);
|
||||
overflow: hidden !important;
|
||||
position: fixed !important;
|
||||
transition-duration: 0.5s, 0.5s;
|
||||
transition-property: opacity, bottom;
|
||||
transition: opacity 0.2s linear, transform 0.25s linear;
|
||||
z-index: 2147483000 !important;
|
||||
}
|
||||
|
||||
|
@ -73,6 +74,7 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
}
|
||||
|
||||
.woot-widget-bubble img {
|
||||
all: revert;
|
||||
height: 24px;
|
||||
margin: 20px;
|
||||
width: 24px;
|
||||
|
@ -110,7 +112,8 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
}
|
||||
|
||||
.woot--hide {
|
||||
bottom: -20000px !important;
|
||||
bottom: -100vh;
|
||||
transform: translateY(40px);
|
||||
top: unset !important;
|
||||
opacity: 0;
|
||||
visibility: hidden !important;
|
||||
|
|
|
@ -25,47 +25,42 @@ export default {
|
|||
default: 'top',
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.focusItem();
|
||||
},
|
||||
methods: {
|
||||
focusItem() {
|
||||
this.$refs.dropdownMenu
|
||||
.querySelector('ul.dropdown li.dropdown-menu__item .button')
|
||||
.focus();
|
||||
dropdownMenuButtons() {
|
||||
return this.$refs.dropdownMenu.querySelectorAll(
|
||||
'ul.dropdown li.dropdown-menu__item .button'
|
||||
);
|
||||
},
|
||||
activeElementIndex() {
|
||||
const menuButtons = this.dropdownMenuButtons();
|
||||
const focusedButton = this.$refs.dropdownMenu.querySelector(
|
||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
const activeIndex = [...menuButtons].indexOf(focusedButton);
|
||||
return activeIndex;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedArrowUpKey(e)) {
|
||||
const items = this.$refs.dropdownMenu.querySelectorAll(
|
||||
'ul.dropdown li.dropdown-menu__item .button'
|
||||
);
|
||||
const focusItems = this.$refs.dropdownMenu.querySelector(
|
||||
'ul.dropdown li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
const activeElementIndex = [...items].indexOf(focusItems);
|
||||
const lastElementIndex = items.length - 1;
|
||||
const menuButtons = this.dropdownMenuButtons();
|
||||
const lastElementIndex = menuButtons.length - 1;
|
||||
|
||||
if (activeElementIndex >= 1) {
|
||||
items[activeElementIndex - 1].focus();
|
||||
if (menuButtons.length === 0) return;
|
||||
|
||||
if (hasPressedArrowUpKey(e)) {
|
||||
const activeIndex = this.activeElementIndex();
|
||||
|
||||
if (activeIndex >= 1) {
|
||||
menuButtons[activeIndex - 1].focus();
|
||||
} else {
|
||||
items[lastElementIndex].focus();
|
||||
menuButtons[lastElementIndex].focus();
|
||||
}
|
||||
}
|
||||
if (hasPressedArrowDownKey(e)) {
|
||||
const items = this.$refs.dropdownMenu.querySelectorAll(
|
||||
'li.dropdown-menu__item .button'
|
||||
);
|
||||
const focusItems = this.$refs.dropdownMenu.querySelector(
|
||||
'li.dropdown-menu__item .button:focus'
|
||||
);
|
||||
const activeElementIndex = [...items].indexOf(focusItems);
|
||||
const lastElementIndex = items.length - 1;
|
||||
const activeIndex = this.activeElementIndex();
|
||||
|
||||
if (activeElementIndex === lastElementIndex) {
|
||||
items[0].focus();
|
||||
if (activeIndex === lastElementIndex) {
|
||||
menuButtons[0].focus();
|
||||
} else {
|
||||
items[activeElementIndex + 1].focus();
|
||||
menuButtons[activeIndex + 1].focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -59,6 +59,16 @@ export default {
|
|||
activeCampaign() {
|
||||
this.setCampaignView();
|
||||
},
|
||||
showUnreadView(newVal) {
|
||||
if (newVal) {
|
||||
this.setIframeHeight(this.isMobile);
|
||||
}
|
||||
},
|
||||
showCampaignView(newVal) {
|
||||
if (newVal) {
|
||||
this.setIframeHeight(this.isMobile);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { websiteToken, locale } = window.chatwootWebChannel;
|
||||
|
@ -98,9 +108,13 @@ export default {
|
|||
});
|
||||
},
|
||||
setIframeHeight(isFixedHeight) {
|
||||
IFrameHelper.sendMessage({
|
||||
event: 'updateIframeHeight',
|
||||
isFixedHeight,
|
||||
this.$nextTick(() => {
|
||||
const extraHeight = this.getExtraSpaceToscroll();
|
||||
IFrameHelper.sendMessage({
|
||||
event: 'updateIframeHeight',
|
||||
isFixedHeight,
|
||||
extraHeight,
|
||||
});
|
||||
});
|
||||
},
|
||||
setLocale(locale) {
|
||||
|
@ -256,6 +270,23 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
getExtraSpaceToscroll: () => {
|
||||
// This function calculates the extra space needed for the view to
|
||||
// accomodate the height of close button + height of
|
||||
// read messages button. So that scrollbar won't appear
|
||||
const unreadMessageWrap = document.querySelector('.unread-messages');
|
||||
const unreadCloseWrap = document.querySelector('.close-unread-wrap');
|
||||
const readViewWrap = document.querySelector('.open-read-view-wrap');
|
||||
|
||||
if (!unreadMessageWrap) return 0;
|
||||
|
||||
// 24px to compensate the paddings
|
||||
let extraHeight = 24 + unreadMessageWrap.scrollHeight;
|
||||
if (unreadCloseWrap) extraHeight += unreadCloseWrap.scrollHeight;
|
||||
if (readViewWrap) extraHeight += readViewWrap.scrollHeight;
|
||||
|
||||
return extraHeight;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -29,6 +29,7 @@ export const actions = {
|
|||
name: userObject.name,
|
||||
avatar_url: userObject.avatar_url,
|
||||
identifier_hash: userObject.identifier_hash,
|
||||
phone_number: userObject.phone_number,
|
||||
};
|
||||
const {
|
||||
data: { pubsub_token: pubsubToken },
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="open-read-view-wrap">
|
||||
<button
|
||||
v-if="unreadMessageCount"
|
||||
class="button clear-button"
|
||||
|
|
|
@ -9,6 +9,7 @@ class Notification::PushNotificationService
|
|||
notification_subscriptions.each do |subscription|
|
||||
send_browser_push(subscription)
|
||||
send_fcm_push(subscription)
|
||||
send_push_via_chatwoot_hub(subscription)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -74,6 +75,18 @@ class Notification::PushNotificationService
|
|||
|
||||
fcm = FCM.new(ENV['FCM_SERVER_KEY'])
|
||||
response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options)
|
||||
remove_subscription_if_error(subscription, response)
|
||||
end
|
||||
|
||||
def send_push_via_chatwoot_hub(subscription)
|
||||
return if ENV['FCM_SERVER_KEY']
|
||||
return unless ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
|
||||
return unless subscription.fcm?
|
||||
|
||||
ChatwootHub.send_browser_push([subscription.subscription_attributes['push_token']], fcm_options)
|
||||
end
|
||||
|
||||
def remove_subscription_if_error(subscription, response)
|
||||
subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
|
||||
end
|
||||
|
||||
|
|
|
@ -43,9 +43,9 @@
|
|||
</label>
|
||||
<div class="update-subscription--checkbox">
|
||||
<%= check_box_tag "subscribe_to_updates", 'true', true %>
|
||||
<div for="subscribe_to_updates">
|
||||
Subscribe to release notes, newsletters & product feedback surveys.
|
||||
</div>
|
||||
<label for="subscribe_to_updates">
|
||||
Subscribe to release notes, newsletters & product feedback surveys.
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="button nice large expanded">
|
||||
Finish Setup
|
||||
|
|
|
@ -39,7 +39,8 @@ window.addEventListener('chatwoot:ready', function() {
|
|||
window.$chatwoot.setUser('<%= user_id %>', {
|
||||
identifier_hash: '<%= user_hash %>',
|
||||
email: 'jane@example.com',
|
||||
name: 'Jane Doe'
|
||||
name: 'Jane Doe',
|
||||
phone_number: ''
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
shared: &shared
|
||||
version: '1.18.1'
|
||||
version: '1.19.0'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
|
|
@ -63,6 +63,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
|||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
||||
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
|
||||
|
||||
RAILS_ENV=production bundle exec rake db:create
|
||||
RAILS_ENV=production bundle exec rake db:reset
|
||||
|
|
|
@ -70,6 +70,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
|||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
||||
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
|
||||
|
||||
RAILS_ENV=production bundle exec rake db:create
|
||||
RAILS_ENV=production bundle exec rake db:reset
|
||||
|
|
|
@ -15,6 +15,7 @@ services:
|
|||
environment:
|
||||
- NODE_ENV=production
|
||||
- RAILS_ENV=production
|
||||
- INSTALLATION_ENV=docker
|
||||
entrypoint: docker/entrypoints/rails.sh
|
||||
command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0']
|
||||
|
||||
|
@ -26,6 +27,7 @@ services:
|
|||
environment:
|
||||
- NODE_ENV=production
|
||||
- RAILS_ENV=production
|
||||
- INSTALLATION_ENV=docker
|
||||
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
|
||||
|
||||
postgres:
|
||||
|
|
|
@ -2,6 +2,7 @@ class ChatwootHub
|
|||
BASE_URL = ENV['CHATWOOT_HUB_URL'] || 'https://hub.2.chatwoot.com'
|
||||
PING_URL = "#{BASE_URL}/ping".freeze
|
||||
REGISTRATION_URL = "#{BASE_URL}/instances".freeze
|
||||
PUSH_NOTIFICATION_URL = "#{BASE_URL}/send_push".freeze
|
||||
EVENTS_URL = "#{BASE_URL}/events".freeze
|
||||
|
||||
def self.installation_identifier
|
||||
|
@ -14,7 +15,8 @@ class ChatwootHub
|
|||
{
|
||||
installation_identifier: installation_identifier,
|
||||
installation_version: Chatwoot.config[:version],
|
||||
installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host
|
||||
installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host,
|
||||
installation_env: ENV.fetch('INSTALLATION_ENV', '')
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -50,7 +52,16 @@ class ChatwootHub
|
|||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||
Rails.logger.info "Exception: #{e.message}"
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
|
||||
def self.send_browser_push(fcm_token_list, fcm_options)
|
||||
info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options }
|
||||
RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||
Rails.logger.info "Exception: #{e.message}"
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
|
||||
def self.emit_event(event_name, event_data)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
module ExceptionList
|
||||
REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest,
|
||||
RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError,
|
||||
RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout, SocketError].freeze
|
||||
RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout,
|
||||
RestClient::MovedPermanently, SocketError].freeze
|
||||
SMTP_EXCEPTIONS = [
|
||||
Net::SMTPSyntaxError
|
||||
].freeze
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "1.18.1",
|
||||
"version": "1.19.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/javascript --fix",
|
||||
|
|
|
@ -45,6 +45,17 @@ describe ::ContactIdentifyAction do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when contact with same phone_number exists' do
|
||||
it 'merges the current contact to phone_number contact' do
|
||||
existing_phone_number_contact = create(:contact, account: account, phone_number: '+919999888877')
|
||||
params = { phone_number: '+919999888877' }
|
||||
result = described_class.new(contact: contact, params: params).perform
|
||||
expect(result.id).to eq existing_phone_number_contact.id
|
||||
expect(result.name).to eq existing_phone_number_contact.name
|
||||
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do
|
||||
it 'updates the attributes of contact passed in to identify action' do
|
||||
create(:contact, account: account, identifier: '')
|
||||
|
|
|
@ -188,6 +188,19 @@ RSpec.describe 'Contacts API', type: :request do
|
|||
expect(response.body).to include(contact2.email)
|
||||
expect(response.body).not_to include(contact1.email)
|
||||
end
|
||||
|
||||
it 'matches the contact respecting the identifier character casing' do
|
||||
contact_normal = create(:contact, name: 'testcontact', account: account, identifier: 'testidentifer')
|
||||
contact_special = create(:contact, name: 'testcontact', account: account, identifier: 'TestIdentifier')
|
||||
get "/api/v1/accounts/#{account.id}/contacts/search",
|
||||
params: { q: 'TestIdentifier' },
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(contact_special.identifier)
|
||||
expect(response.body).not_to include(contact_normal.identifier)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -284,7 +297,7 @@ RSpec.describe 'Contacts API', type: :request do
|
|||
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
|
||||
end
|
||||
|
||||
it 'creates the contact identifier when inbox id is passed' do
|
||||
it 'creates the contact inbox when inbox id is passed' do
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
||||
params: valid_params.merge({ inbox_id: inbox.id })
|
||||
|
|
|
@ -20,17 +20,20 @@ describe Notification::PushNotificationService do
|
|||
described_class.new(notification: notification).perform
|
||||
expect(Webpush).to have_received(:payload_send)
|
||||
expect(FCM).not_to have_received(:new)
|
||||
ENV['ENABLE_ACCOUNT_SIGNUP'] = nil
|
||||
ENV['VAPID_PUBLIC_KEY'] = nil
|
||||
end
|
||||
|
||||
it 'sends a fcm notification for firebase subscription' do
|
||||
ENV['FCM_SERVER_KEY'] = 'test'
|
||||
ENV['ENABLE_PUSH_RELAY_SERVER'] = 'false'
|
||||
create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(FCM).to have_received(:new)
|
||||
expect(Webpush).not_to have_received(:payload_send)
|
||||
ENV['ENABLE_ACCOUNT_SIGNUP'] = nil
|
||||
|
||||
ENV['FCM_SERVER_KEY'] = nil
|
||||
ENV['ENABLE_PUSH_RELAY_SERVER'] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,13 @@ properties:
|
|||
required: true
|
||||
name:
|
||||
type: string
|
||||
description: name of the contact
|
||||
email:
|
||||
type: string
|
||||
description: email of the contact
|
||||
phone_number:
|
||||
type: string
|
||||
description: phone number of the contact
|
||||
identifier:
|
||||
type: string
|
||||
description: A unique identifier for the contact in external system
|
||||
|
|
|
@ -2,7 +2,13 @@ type: object
|
|||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: name of the contact
|
||||
email:
|
||||
type: string
|
||||
description: email of the contact
|
||||
phone_number:
|
||||
type: string
|
||||
description: phone number of the contact
|
||||
identifier:
|
||||
type: string
|
||||
description: A unique identifier for the contact in external system
|
||||
|
|
|
@ -8,6 +8,7 @@ get:
|
|||
- name: q
|
||||
in: query
|
||||
type: string
|
||||
description: Search using contact `name`, `identifier`, `email` or `phone number`
|
||||
- $ref: '#/parameters/contact_sort_param'
|
||||
- $ref: '#/parameters/page'
|
||||
responses:
|
||||
|
|
|
@ -1281,7 +1281,8 @@
|
|||
{
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Search using contact `name`, `identifier`, `email` or `phone number`"
|
||||
},
|
||||
{
|
||||
"$ref": "#/parameters/contact_sort_param"
|
||||
|
@ -3376,13 +3377,20 @@
|
|||
"required": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "name of the contact"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "email of the contact"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "phone number of the contact"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"description": "A unique identifier for the contact in external system"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3390,13 +3398,20 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "name of the contact"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "email of the contact"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "phone number of the contact"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"description": "A unique identifier for the contact in external system"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue