Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
0f79135f20
88 changed files with 581 additions and 421 deletions
|
@ -7,8 +7,8 @@ end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = spaces
|
indent_style = space
|
||||||
tab_width = 2
|
tab_width = 2
|
||||||
|
|
||||||
[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
|
[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
2
.github/workflows/publish_foss_docker.yml
vendored
2
.github/workflows/publish_foss_docker.yml
vendored
|
@ -58,6 +58,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.DOCKER_TAG }}
|
tags: ${{ env.DOCKER_TAG }}
|
||||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -39,9 +39,6 @@ public/packs*
|
||||||
*.un~
|
*.un~
|
||||||
.jest-cache
|
.jest-cache
|
||||||
|
|
||||||
#VS Code files
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
# ignore jetbrains IDE files
|
# ignore jetbrains IDE files
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
@ -62,4 +59,4 @@ package-lock.json
|
||||||
test/cypress/videos/*
|
test/cypress/videos/*
|
||||||
|
|
||||||
/config/master.key
|
/config/master.key
|
||||||
/config/*.enc
|
/config/*.enc
|
||||||
|
|
|
@ -16,6 +16,7 @@ Metrics/ClassLength:
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/builders/messages/facebook/message_builder.rb'
|
- 'app/builders/messages/facebook/message_builder.rb'
|
||||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||||
|
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
|
||||||
- 'app/listeners/action_cable_listener.rb'
|
- 'app/listeners/action_cable_listener.rb'
|
||||||
- 'app/models/conversation.rb'
|
- 'app/models/conversation.rb'
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
|
|
32
.vscode/extensions.json
vendored
Normal file
32
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Spell check
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
// Better Comments
|
||||||
|
"aaron-bond.better-comments",
|
||||||
|
// Rails Test Runner
|
||||||
|
"davidpallinder.rails-test-runner",
|
||||||
|
// Eslint
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
// Auto Close Tag
|
||||||
|
"formulahendry.auto-close-tag",
|
||||||
|
// Auto Rename Tag
|
||||||
|
"formulahendry.auto-rename-tag",
|
||||||
|
// Hight light colors
|
||||||
|
"naumovs.color-highlight",
|
||||||
|
// GitLens
|
||||||
|
"eamodio.gitlens",
|
||||||
|
// Ruby
|
||||||
|
"rebornix.ruby",
|
||||||
|
// Vue
|
||||||
|
"octref.vetur",
|
||||||
|
// Prettier
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
// Dot Env
|
||||||
|
"mikestead.dotenv",
|
||||||
|
// HTML CSS Support
|
||||||
|
"ecmel.vscode-html-css",
|
||||||
|
// Tailwind CSS Intellisense
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
|
# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned.
|
||||||
|
# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
|
||||||
|
|
||||||
class ContactInboxBuilder
|
class ContactInboxBuilder
|
||||||
pattr_initialize [:contact_id!, :inbox_id!, :source_id]
|
pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@contact = Contact.find(contact_id)
|
@source_id ||= generate_source_id
|
||||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
create_contact_inbox if source_id.present?
|
||||||
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
|
||||||
|
|
||||||
source_id = @source_id || generate_source_id
|
|
||||||
create_contact_inbox(source_id) if source_id.present?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -19,23 +18,37 @@ class ContactInboxBuilder
|
||||||
when 'Channel::Whatsapp'
|
when 'Channel::Whatsapp'
|
||||||
wa_source_id
|
wa_source_id
|
||||||
when 'Channel::Email'
|
when 'Channel::Email'
|
||||||
@contact.email
|
email_source_id
|
||||||
when 'Channel::Sms'
|
when 'Channel::Sms'
|
||||||
@contact.phone_number
|
phone_source_id
|
||||||
when 'Channel::Api'
|
when 'Channel::Api', 'Channel::WebWidget'
|
||||||
SecureRandom.uuid
|
SecureRandom.uuid
|
||||||
|
else
|
||||||
|
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_source_id
|
||||||
|
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
|
||||||
|
|
||||||
|
@contact.email
|
||||||
|
end
|
||||||
|
|
||||||
|
def phone_source_id
|
||||||
|
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||||
|
|
||||||
|
@contact.phone_number
|
||||||
|
end
|
||||||
|
|
||||||
def wa_source_id
|
def wa_source_id
|
||||||
return unless @contact.phone_number
|
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||||
|
|
||||||
# whatsapp doesn't want the + in e164 format
|
# whatsapp doesn't want the + in e164 format
|
||||||
@contact.phone_number.delete('+').to_s
|
@contact.phone_number.delete('+').to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def twilio_source_id
|
def twilio_source_id
|
||||||
return unless @contact.phone_number
|
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||||
|
|
||||||
case @inbox.channel.medium
|
case @inbox.channel.medium
|
||||||
when 'sms'
|
when 'sms'
|
||||||
|
@ -45,11 +58,11 @@ class ContactInboxBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(source_id)
|
def create_contact_inbox
|
||||||
::ContactInbox.find_or_create_by!(
|
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
||||||
contact_id: @contact.id,
|
contact_id: @contact.id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
source_id: source_id
|
source_id: @source_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,25 +1,47 @@
|
||||||
class ContactBuilder
|
# This Builder will create a contact and contact inbox with specified attributes.
|
||||||
pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified]
|
# If an existing identified contact exisits, it will be returned.
|
||||||
|
# for contact inbox logic it uses the contact inbox builder
|
||||||
|
|
||||||
|
class ContactInboxWithContactBuilder
|
||||||
|
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
|
find_or_create_contact_and_contact_inbox
|
||||||
return contact_inbox if contact_inbox
|
# in case of race conditions where contact is created by another thread
|
||||||
|
# we will try to find the contact and create a contact inbox
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
find_or_create_contact_and_contact_inbox
|
||||||
|
end
|
||||||
|
|
||||||
build_contact_inbox
|
def find_or_create_contact_and_contact_inbox
|
||||||
|
@contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present?
|
||||||
|
return @contact_inbox if @contact_inbox
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction(requires_new: true) do
|
||||||
|
build_contact_with_contact_inbox
|
||||||
|
update_contact_avatar(@contact) unless @contact.avatar.attached?
|
||||||
|
@contact_inbox
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_contact_with_contact_inbox
|
||||||
|
@contact = find_contact || create_contact
|
||||||
|
@contact_inbox = create_contact_inbox
|
||||||
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
@account ||= inbox.account
|
@account ||= inbox.account
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(contact)
|
def create_contact_inbox
|
||||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
ContactInboxBuilder.new(
|
||||||
contact_id: contact.id,
|
contact: @contact,
|
||||||
inbox_id: inbox.id,
|
inbox: @inbox,
|
||||||
source_id: source_id
|
source_id: @source_id,
|
||||||
)
|
hmac_verified: hmac_verified
|
||||||
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_contact_avatar(contact)
|
def update_contact_avatar(contact)
|
||||||
|
@ -61,16 +83,4 @@ class ContactBuilder
|
||||||
|
|
||||||
account.contacts.find_by(phone_number: phone_number)
|
account.contacts.find_by(phone_number: phone_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
contact = find_contact || create_contact
|
|
||||||
contact_inbox = create_contact_inbox(contact)
|
|
||||||
update_contact_avatar(contact)
|
|
||||||
contact_inbox
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
|
@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
return if @inbox.channel.reauthorization_required?
|
return if @inbox.channel.reauthorization_required?
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_contact
|
build_contact_inbox
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
ensure_contact_avatar
|
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def contact
|
def build_contact_inbox
|
||||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
end
|
source_id: @sender_id,
|
||||||
|
inbox: @inbox,
|
||||||
def build_contact
|
contact_attributes: contact_params
|
||||||
return if contact.present?
|
).perform
|
||||||
|
|
||||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
|
||||||
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
|
@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_contact_avatar
|
|
||||||
return if contact_params[:remote_avatar_url].blank?
|
|
||||||
return if @contact.avatar.attached?
|
|
||||||
|
|
||||||
Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url])
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_conversation
|
def build_conversation
|
||||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
|
|
||||||
Conversation.create!(conversation_params.merge(
|
Conversation.create!(conversation_params.merge(
|
||||||
contact_inbox_id: @contact_inbox.id
|
contact_inbox_id: @contact_inbox.id
|
||||||
))
|
))
|
||||||
|
@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
{
|
{
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
contact_id: contact.id
|
contact_id: @contact_inbox.contact_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
message_type: @message_type,
|
message_type: @message_type,
|
||||||
content: response.content,
|
content: response.content,
|
||||||
source_id: response.identifier,
|
source_id: response.identifier,
|
||||||
sender: @outgoing_echo ? nil : contact
|
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
{
|
{
|
||||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
remote_avatar_url: result['profile_pic'] || ''
|
avatar_url: result['profile_pic']
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
|
||||||
before_action :ensure_inbox, only: [:create]
|
before_action :ensure_inbox, only: [:create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
@contact_inbox = ContactInboxBuilder.new(
|
||||||
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
|
contact: @contact,
|
||||||
|
inbox: @inbox,
|
||||||
|
source_id: params[:source_id]
|
||||||
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
return if params[:inbox_id].blank?
|
return if params[:inbox_id].blank?
|
||||||
|
|
||||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
ContactInboxBuilder.new(
|
||||||
ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id)
|
contact: @contact,
|
||||||
|
inbox: inbox,
|
||||||
|
source_id: params[:source_id]
|
||||||
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
|
|
|
@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
include DateRangeHelper
|
include DateRangeHelper
|
||||||
|
|
||||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
||||||
before_action :contact_inbox, only: [:create]
|
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
result = conversation_finder.perform
|
result = conversation_finder.perform
|
||||||
|
@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
authorize @conversation.inbox, :show?
|
authorize @conversation.inbox, :show?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
return if params[:inbox_id].blank?
|
||||||
|
|
||||||
|
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
|
authorize @inbox, :show?
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact
|
||||||
|
return if params[:contact_id].blank?
|
||||||
|
|
||||||
|
@contact = Current.account.contacts.find(params[:contact_id])
|
||||||
|
end
|
||||||
|
|
||||||
def contact_inbox
|
def contact_inbox
|
||||||
@contact_inbox = build_contact_inbox
|
@contact_inbox = build_contact_inbox
|
||||||
|
|
||||||
|
# fallback for the old case where we do look up only using source id
|
||||||
|
# In future we need to change this and make sure we do look up on combination of inbox_id and source_id
|
||||||
|
# and deprecate the support of passing only source_id as the param
|
||||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||||
authorize @contact_inbox.inbox, :show?
|
authorize @contact_inbox.inbox, :show?
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
return if params[:contact_id].blank? || params[:inbox_id].blank?
|
return if @inbox.blank? || @contact.blank?
|
||||||
|
|
||||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
|
||||||
authorize inbox, :show?
|
|
||||||
|
|
||||||
ContactInboxBuilder.new(
|
ContactInboxBuilder.new(
|
||||||
contact_id: params[:contact_id],
|
contact: @contact,
|
||||||
inbox_id: inbox.id,
|
inbox: @inbox,
|
||||||
source_id: params[:source_id]
|
source_id: params[:source_id]
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,8 @@ module RequestExceptionHandler
|
||||||
render_not_found_error('Resource could not be found')
|
render_not_found_error('Resource could not be found')
|
||||||
rescue Pundit::NotAuthorizedError
|
rescue Pundit::NotAuthorizedError
|
||||||
render_unauthorized('You are not authorized to do this action')
|
render_unauthorized('You are not authorized to do this action')
|
||||||
|
rescue ActionController::ParameterMissing => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
ensure
|
ensure
|
||||||
# to address the thread variable leak issues in Puma/Thin webserver
|
# to address the thread variable leak issues in Puma/Thin webserver
|
||||||
Current.reset
|
Current.reset
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||||
|
|
||||||
def create
|
def create
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
source_id = params[:source_id] || SecureRandom.uuid
|
||||||
@contact_inbox = ::ContactBuilder.new(
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: source_id,
|
source_id: source_id,
|
||||||
inbox: @inbox_channel.inbox,
|
inbox: @inbox_channel.inbox,
|
||||||
contact_attributes: permitted_params.except(:identifier, :identifier_hash)
|
contact_attributes: permitted_params.except(:identifier, :identifier_hash)
|
||||||
|
|
|
@ -113,9 +113,22 @@ $default-button-height: 4.0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.clear {
|
&.clear {
|
||||||
|
color: var(--w-700);
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
color: var(--s-700)
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: var(--g-700)
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alert {
|
||||||
|
color: var(--r-700)
|
||||||
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
color: var(--y-600);
|
color: var(--y-700)
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -146,6 +159,8 @@ $default-button-height: 4.0rem;
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
height: var(--space-large);
|
height: var(--space-large);
|
||||||
|
padding-bottom: var(--space-smaller);
|
||||||
|
padding-top: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
&.large {
|
||||||
|
|
|
@ -14,15 +14,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal--close {
|
.modal--close {
|
||||||
border-radius: 50%;
|
|
||||||
color: $color-heading;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: $font-size-big;
|
|
||||||
line-height: $space-normal;
|
|
||||||
padding: $space-normal;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: $space-micro;
|
right: $space-small;
|
||||||
top: $space-micro;
|
top: $space-small;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $color-background;
|
background: $color-background;
|
||||||
|
|
|
@ -7,9 +7,13 @@
|
||||||
@click="onBackDropClick"
|
@click="onBackDropClick"
|
||||||
>
|
>
|
||||||
<div :class="modalContainerClassName" @click.stop>
|
<div :class="modalContainerClassName" @click.stop>
|
||||||
<button class="modal--close" @click="close">
|
<woot-button
|
||||||
<fluent-icon icon="dismiss" />
|
color-scheme="secondary"
|
||||||
</button>
|
icon="dismiss"
|
||||||
|
variant="clear"
|
||||||
|
class="modal--close"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,7 +39,7 @@ const primaryMenuItems = accountId => [
|
||||||
label: 'HELP_CENTER.TITLE',
|
label: 'HELP_CENTER.TITLE',
|
||||||
featureFlag: 'help_center',
|
featureFlag: 'help_center',
|
||||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||||
toStateName: 'list_all_portals',
|
toStateName: 'default_portal_articles',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -78,7 +78,7 @@ export default {
|
||||||
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
||||||
initials = initials.replace(/[a-z]+/g, '');
|
initials = initials.replace(/[a-z]+/g, '');
|
||||||
}
|
}
|
||||||
initials = initials.substr(0, 2).toUpperCase();
|
initials = initials.substring(0, 2).toUpperCase();
|
||||||
return initials;
|
return initials;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -420,6 +420,8 @@ export default {
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.wrap {
|
.wrap {
|
||||||
> .bubble {
|
> .bubble {
|
||||||
|
min-width: 128px;
|
||||||
|
|
||||||
&.is-image,
|
&.is-image,
|
||||||
&.is-video {
|
&.is-video {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -477,7 +477,7 @@ export default {
|
||||||
const hasNextWord = updatedMessage.includes(' ');
|
const hasNextWord = updatedMessage.includes(' ');
|
||||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||||
if (isShortCodeActive) {
|
if (isShortCodeActive) {
|
||||||
this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length);
|
this.mentionSearchKey = updatedMessage.substring(1);
|
||||||
this.showMentions = true;
|
this.showMentions = true;
|
||||||
} else {
|
} else {
|
||||||
this.mentionSearchKey = '';
|
this.mentionSearchKey = '';
|
||||||
|
|
|
@ -12,12 +12,12 @@
|
||||||
"404": "Šim kontam nav piesaistīts neviens aģents",
|
"404": "Šim kontam nav piesaistīts neviens aģents",
|
||||||
"TITLE": "Pārvaldīt Jūsu komandas aģentus",
|
"TITLE": "Pārvaldīt Jūsu komandas aģentus",
|
||||||
"DESC": "Jūs varat pievienot/noņemt aģentus pie/no savas komandas.",
|
"DESC": "Jūs varat pievienot/noņemt aģentus pie/no savas komandas.",
|
||||||
"NAME": "Vārds",
|
"NAME": "Nosaukums",
|
||||||
"EMAIL": "e-pasts",
|
"EMAIL": "Epasts",
|
||||||
"STATUS": "Statuss",
|
"STATUS": "Statuss",
|
||||||
"ACTIONS": "Darbības",
|
"ACTIONS": "Darbības",
|
||||||
"VERIFIED": "Pārbaudīts",
|
"VERIFIED": "Pārbaudīts",
|
||||||
"VERIFICATION_PENDING": "Gaida apstiprinājumu"
|
"VERIFICATION_PENDING": "Tiek gaidīta verifikācija"
|
||||||
},
|
},
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Pievienot aģentu Jūsu komandai",
|
"TITLE": "Pievienot aģentu Jūsu komandai",
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
},
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"TABLE_HEADER": [
|
"TABLE_HEADER": [
|
||||||
"Vārds",
|
"Nosaukums",
|
||||||
"Apraksts",
|
"Apraksts",
|
||||||
"Tips",
|
"Tips",
|
||||||
"Atslēga"
|
"Atslēga"
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
},
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"TABLE_HEADER": [
|
"TABLE_HEADER": [
|
||||||
"Vārds",
|
"Nosaukums",
|
||||||
"Apraksts",
|
"Apraksts",
|
||||||
"Aktīvs",
|
"Aktīvs",
|
||||||
"Izveidots"
|
"Izveidots"
|
||||||
|
|
|
@ -202,7 +202,7 @@
|
||||||
"404": "Neviena kontaktpersona neatbilst jūsu meklēšanas vaicājumam 🔍",
|
"404": "Neviena kontaktpersona neatbilst jūsu meklēšanas vaicājumam 🔍",
|
||||||
"NO_CONTACTS": "Nav pieejamu kontaktpersonu",
|
"NO_CONTACTS": "Nav pieejamu kontaktpersonu",
|
||||||
"TABLE_HEADER": {
|
"TABLE_HEADER": {
|
||||||
"NAME": "Vārds",
|
"NAME": "Nosaukums",
|
||||||
"PHONE_NUMBER": "Telefona Numurs",
|
"PHONE_NUMBER": "Telefona Numurs",
|
||||||
"CONVERSATIONS": "Sarunas",
|
"CONVERSATIONS": "Sarunas",
|
||||||
"LAST_ACTIVITY": "Pēdējās Darbības",
|
"LAST_ACTIVITY": "Pēdējās Darbības",
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"days_before": "Ir x dienas pirms"
|
"days_before": "Ir x dienas pirms"
|
||||||
},
|
},
|
||||||
"ATTRIBUTES": {
|
"ATTRIBUTES": {
|
||||||
"NAME": "Vārds",
|
"NAME": "Nosaukums",
|
||||||
"EMAIL": "E-pasts",
|
"EMAIL": "E-pasts",
|
||||||
"PHONE_NUMBER": "Tālruņa numurs",
|
"PHONE_NUMBER": "Tālruņa numurs",
|
||||||
"IDENTIFIER": "Identifikators",
|
"IDENTIFIER": "Identifikators",
|
||||||
|
|
|
@ -151,7 +151,7 @@
|
||||||
"CONTEXT_MENU": {
|
"CONTEXT_MENU": {
|
||||||
"COPY": "Kopēt",
|
"COPY": "Kopēt",
|
||||||
"DELETE": "Dzēst",
|
"DELETE": "Dzēst",
|
||||||
"CREATE_A_CANNED_RESPONSE": "Add to canned responses"
|
"CREATE_A_CANNED_RESPONSE": "Pievienot sagatavotajām atbildēm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EMAIL_TRANSCRIPT": {
|
"EMAIL_TRANSCRIPT": {
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
"LOADING_MESSAGE": "Notiek paziņojumu ielāde...",
|
"LOADING_MESSAGE": "Notiek paziņojumu ielāde...",
|
||||||
"404": "Nav paziņojumu",
|
"404": "Nav paziņojumu",
|
||||||
"TABLE_HEADER": [
|
"TABLE_HEADER": [
|
||||||
"Vārds",
|
"Nosaukums",
|
||||||
"Telefona numurs",
|
"Telefona numurs",
|
||||||
"Sarunas",
|
"Sarunas",
|
||||||
"Pēdējā Sazināšanās"
|
"Pēdējā Sazināšanās"
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
"PORTAL_CONFIG": {
|
"PORTAL_CONFIG": {
|
||||||
"TITLE": "Portāla Konfigurācijas",
|
"TITLE": "Portāla Konfigurācijas",
|
||||||
"ITEMS": {
|
"ITEMS": {
|
||||||
"NAME": "Vārds",
|
"NAME": "Nosaukums",
|
||||||
"DOMAIN": "Pielāgots domēns",
|
"DOMAIN": "Pielāgots domēns",
|
||||||
"SLUG": "Slug",
|
"SLUG": "Slug",
|
||||||
"TITLE": "Portāla nosaukums",
|
"TITLE": "Portāla nosaukums",
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
"TITLE": "Kategorijas iekš",
|
"TITLE": "Kategorijas iekš",
|
||||||
"NEW_CATEGORY": "Jauna kategorija",
|
"NEW_CATEGORY": "Jauna kategorija",
|
||||||
"TABLE": {
|
"TABLE": {
|
||||||
"NAME": "Vārds",
|
"NAME": "Nosaukums",
|
||||||
"DESCRIPTION": "Apraksts",
|
"DESCRIPTION": "Apraksts",
|
||||||
"LOCALE": "Lokalizācija",
|
"LOCALE": "Lokalizācija",
|
||||||
"ARTICLE_COUNT": "Rakstu skaits",
|
"ARTICLE_COUNT": "Rakstu skaits",
|
||||||
|
@ -204,7 +204,7 @@
|
||||||
"HELP_TEXT": "Šis logotips tiks attēlots portāla galvenē."
|
"HELP_TEXT": "Šis logotips tiks attēlots portāla galvenē."
|
||||||
},
|
},
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Vārds",
|
"LABEL": "Nosaukums",
|
||||||
"PLACEHOLDER": "Portāla nosaukums",
|
"PLACEHOLDER": "Portāla nosaukums",
|
||||||
"HELP_TEXT": "Nosaukums tiks izmantots publiskajā portālā iekšēji.",
|
"HELP_TEXT": "Nosaukums tiks izmantots publiskajā portālā iekšēji.",
|
||||||
"ERROR": "Nepieciešams nosaukums"
|
"ERROR": "Nepieciešams nosaukums"
|
||||||
|
@ -344,7 +344,7 @@
|
||||||
"PORTAL": "Portāls",
|
"PORTAL": "Portāls",
|
||||||
"LOCALE": "Lokalizācija",
|
"LOCALE": "Lokalizācija",
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Vārds",
|
"LABEL": "Nosaukums",
|
||||||
"PLACEHOLDER": "Kategorijas nosaukums",
|
"PLACEHOLDER": "Kategorijas nosaukums",
|
||||||
"HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.",
|
"HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.",
|
||||||
"ERROR": "Nepieciešams nosaukums"
|
"ERROR": "Nepieciešams nosaukums"
|
||||||
|
@ -375,7 +375,7 @@
|
||||||
"PORTAL": "Portāls",
|
"PORTAL": "Portāls",
|
||||||
"LOCALE": "Lokalizācija",
|
"LOCALE": "Lokalizācija",
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Vārds",
|
"LABEL": "Nosaukums",
|
||||||
"PLACEHOLDER": "Kategorijas nosaukums",
|
"PLACEHOLDER": "Kategorijas nosaukums",
|
||||||
"HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.",
|
"HELP_TEXT": "Kategorijas nosaukums tiks izmantots publiskajā portālā, lai klasificētu rakstus.",
|
||||||
"ERROR": "Nepieciešams nosaukums"
|
"ERROR": "Nepieciešams nosaukums"
|
||||||
|
|
|
@ -239,7 +239,9 @@
|
||||||
},
|
},
|
||||||
"API_CALLBACK": {
|
"API_CALLBACK": {
|
||||||
"TITLE": "Atzvanīšanas URL",
|
"TITLE": "Atzvanīšanas URL",
|
||||||
"SUBTITLE": "Jums ir facebook izstrādātāju portālā jānokonfigurē webhook URL, izmantojot šeit minēto URL."
|
"SUBTITLE": "Jums Facebook izstrādātāju portālā ir jānokonfigurē webhoot URL un verifikācijas token ar tālāk norādītajām vērtībām.",
|
||||||
|
"WEBHOOK_URL": "Webhook URL",
|
||||||
|
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verifikācijas Token"
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "Izveidot WhatsApp kanālu",
|
"SUBMIT_BUTTON": "Izveidot WhatsApp kanālu",
|
||||||
"API": {
|
"API": {
|
||||||
|
@ -357,7 +359,7 @@
|
||||||
},
|
},
|
||||||
"FINISH": {
|
"FINISH": {
|
||||||
"TITLE": "Jūsu Iesūtne ir gatava!",
|
"TITLE": "Jūsu Iesūtne ir gatava!",
|
||||||
"MESSAGE": "Tagad Jūs varat izmantot savu jauno Kanālu lai sazinātos ar saviem klientiem. Priecīgu atbalstīšanu ",
|
"MESSAGE": "Tagad Jūs varat izmantot savu jauno Kanālu lai sazinātos ar saviem klientiem. Priecīgu atbalstīšanu",
|
||||||
"BUTTON_TEXT": "Iet uz",
|
"BUTTON_TEXT": "Iet uz",
|
||||||
"MORE_SETTINGS": "Papildu iestatījumi",
|
"MORE_SETTINGS": "Papildu iestatījumi",
|
||||||
"WEBSITE_SUCCESS": "Jūs esat veiksmīgi pabeidzis tīmekļa vietnes kanāla izveidi. Nokopējiet tālāk redzamo kodu un ievietojiet to savā tīmekļa vietnē. Nākamreiz, kad klients izmantos tiešsaistes tērzēšanu, saruna automātiski tiks parādīta Jūsu iesūtnē."
|
"WEBSITE_SUCCESS": "Jūs esat veiksmīgi pabeidzis tīmekļa vietnes kanāla izveidi. Nokopējiet tālāk redzamo kodu un ievietojiet to savā tīmekļa vietnē. Nākamreiz, kad klients izmantos tiešsaistes tērzēšanu, saruna automātiski tiks parādīta Jūsu iesūtnē."
|
||||||
|
|
|
@ -94,14 +94,14 @@
|
||||||
"404": "Šajā kontā vēl nav nokonfigurēta neviena informācijas paneļa lietotne",
|
"404": "Šajā kontā vēl nav nokonfigurēta neviena informācijas paneļa lietotne",
|
||||||
"LOADING": "Notiek informācijas paneļa lietotņu iegūšana...",
|
"LOADING": "Notiek informācijas paneļa lietotņu iegūšana...",
|
||||||
"TABLE_HEADER": [
|
"TABLE_HEADER": [
|
||||||
"Vārds",
|
"Nosaukums",
|
||||||
"Endpoint"
|
"Endpoint"
|
||||||
],
|
],
|
||||||
"EDIT_TOOLTIP": "Rediģēt lietotni",
|
"EDIT_TOOLTIP": "Rediģēt lietotni",
|
||||||
"DELETE_TOOLTIP": "Dzēst lietotni"
|
"DELETE_TOOLTIP": "Dzēst lietotni"
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"TITLE_LABEL": "Vārds",
|
"TITLE_LABEL": "Nosaukums",
|
||||||
"TITLE_PLACEHOLDER": "Ievadiet informācijas paneļa lietotnes nosaukumu",
|
"TITLE_PLACEHOLDER": "Ievadiet informācijas paneļa lietotnes nosaukumu",
|
||||||
"TITLE_ERROR": "Informācijas paneļa lietotnei ir jānorāda nosaukums",
|
"TITLE_ERROR": "Informācijas paneļa lietotnei ir jānorāda nosaukums",
|
||||||
"URL_LABEL": "Endpoint",
|
"URL_LABEL": "Endpoint",
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"TITLE": "Pārvaldīt Etiķetes",
|
"TITLE": "Pārvaldīt Etiķetes",
|
||||||
"DESC": "Etiķetes ļauj grupēt sarunas kopā.",
|
"DESC": "Etiķetes ļauj grupēt sarunas kopā.",
|
||||||
"TABLE_HEADER": [
|
"TABLE_HEADER": [
|
||||||
"Vārds",
|
"Nosaukums",
|
||||||
"Apraksts",
|
"Apraksts",
|
||||||
"Krāsa"
|
"Krāsa"
|
||||||
]
|
]
|
||||||
|
|
|
@ -179,6 +179,7 @@
|
||||||
"CONTACTS": "Kontaktpersonas",
|
"CONTACTS": "Kontaktpersonas",
|
||||||
"HOME": "Sākums",
|
"HOME": "Sākums",
|
||||||
"AGENTS": "Aģenti",
|
"AGENTS": "Aģenti",
|
||||||
|
"AGENT_BOTS": "Bots",
|
||||||
"INBOXES": "Iesūtnes",
|
"INBOXES": "Iesūtnes",
|
||||||
"NOTIFICATIONS": "Paziņojumi",
|
"NOTIFICATIONS": "Paziņojumi",
|
||||||
"CANNED_RESPONSES": "Sagatavotās Atbildes",
|
"CANNED_RESPONSES": "Sagatavotās Atbildes",
|
||||||
|
@ -189,6 +190,7 @@
|
||||||
"LABELS": "Etiķetes",
|
"LABELS": "Etiķetes",
|
||||||
"CUSTOM_ATTRIBUTES": "Pielāgotas Īpašības",
|
"CUSTOM_ATTRIBUTES": "Pielāgotas Īpašības",
|
||||||
"AUTOMATION": "Automatizācija",
|
"AUTOMATION": "Automatizācija",
|
||||||
|
"MACROS": "Macros",
|
||||||
"TEAMS": "Komandas",
|
"TEAMS": "Komandas",
|
||||||
"BILLING": "Norēķini",
|
"BILLING": "Norēķini",
|
||||||
"CUSTOM_VIEWS_FOLDER": "Mapes",
|
"CUSTOM_VIEWS_FOLDER": "Mapes",
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
},
|
},
|
||||||
"AGENTS": {
|
"AGENTS": {
|
||||||
"AGENT": "AĢENTS",
|
"AGENT": "AĢENTS",
|
||||||
"EMAIL": "e-pasts",
|
"EMAIL": "Epasts",
|
||||||
"BUTTON_TEXT": "Pievienot aģentus",
|
"BUTTON_TEXT": "Pievienot aģentus",
|
||||||
"ADD_AGENTS": "Notiek aģentu pievienošana Jūsu komandai...",
|
"ADD_AGENTS": "Notiek aģentu pievienošana Jūsu komandai...",
|
||||||
"SELECT": "izvēlēties",
|
"SELECT": "izvēlēties",
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
"BULK_ACTION": {
|
"BULK_ACTION": {
|
||||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversas selecionadas",
|
"CONVERSATIONS_SELECTED": "%{conversationCount} conversas selecionadas",
|
||||||
"AGENT_SELECT_LABEL": "Selecione Agente",
|
"AGENT_SELECT_LABEL": "Selecione Agente",
|
||||||
"ASSIGN_CONFIRMATION_LABEL": "Você tem certeza que deseja atribuir %{conversationCount} %{conversationLabel} para",
|
"ASSIGN_CONFIRMATION_LABEL": "Você tem certeza que quer atribuir %{conversationCount} %{conversationLabel} para",
|
||||||
|
"UNASSIGN_CONFIRMATION_LABEL": "Você tem certeza que quer remover a atribuição de %{conversationCount} %{conversationLabel}?",
|
||||||
"GO_BACK_LABEL": "Voltar atrás",
|
"GO_BACK_LABEL": "Voltar atrás",
|
||||||
"ASSIGN_LABEL": "Atribua",
|
"ASSIGN_LABEL": "Atribua",
|
||||||
|
"YES": "Sim",
|
||||||
"ASSIGN_AGENT_TOOLTIP": "Atribuir Agente",
|
"ASSIGN_AGENT_TOOLTIP": "Atribuir Agente",
|
||||||
"ASSIGN_SUCCESFUL": "Conversas atribuídas com sucesso",
|
"ASSIGN_SUCCESFUL": "Conversas atribuídas com sucesso",
|
||||||
"ASSIGN_FAILED": "Falha ao atribuir conversas, por favor, tente novamente",
|
"ASSIGN_FAILED": "Falha ao atribuir conversas, por favor, tente novamente",
|
||||||
|
|
|
@ -12,9 +12,9 @@
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
<woot-button
|
<woot-button
|
||||||
icon="more-vertical"
|
icon="more-vertical"
|
||||||
class="button--delete-message"
|
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
variant="link"
|
variant="clear"
|
||||||
|
size="small"
|
||||||
@click="handleContextMenuClick"
|
@click="handleContextMenuClick"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
@ -40,6 +40,7 @@
|
||||||
variant="clear"
|
variant="clear"
|
||||||
size="small"
|
size="small"
|
||||||
icon="clipboard"
|
icon="clipboard"
|
||||||
|
color-scheme="secondary"
|
||||||
@click="handleCopy"
|
@click="handleCopy"
|
||||||
>
|
>
|
||||||
{{ $t('CONVERSATION.CONTEXT_MENU.COPY') }}
|
{{ $t('CONVERSATION.CONTEXT_MENU.COPY') }}
|
||||||
|
@ -52,6 +53,7 @@
|
||||||
variant="clear"
|
variant="clear"
|
||||||
size="small"
|
size="small"
|
||||||
icon="comment-add"
|
icon="comment-add"
|
||||||
|
color-scheme="secondary"
|
||||||
@click="showCannedResponseModal"
|
@click="showCannedResponseModal"
|
||||||
>
|
>
|
||||||
{{ $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE') }}
|
{{ $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE') }}
|
||||||
|
@ -132,12 +134,12 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dropdown-pane {
|
.dropdown-pane {
|
||||||
bottom: var(--space-medium);
|
bottom: var(--space-large);
|
||||||
}
|
}
|
||||||
.dropdown-pane--left {
|
.dropdown-pane--left {
|
||||||
right: var(--space-small);
|
right: var(--space-minus-small);
|
||||||
}
|
}
|
||||||
.dropdown-pane--right {
|
.dropdown-pane--right {
|
||||||
left: var(--space-small);
|
left: var(--space-minus-small);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,10 +10,11 @@
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showCopy"
|
v-if="showCopy"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="link"
|
variant="clear"
|
||||||
|
size="tiny"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
icon="clipboard"
|
icon="clipboard"
|
||||||
class-names="icon copy-icon"
|
class-names="copy-icon"
|
||||||
@click="onCopy"
|
@click="onCopy"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -239,7 +239,7 @@ export default {
|
||||||
const hasNextWord = value.includes(' ');
|
const hasNextWord = value.includes(' ');
|
||||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||||
if (isShortCodeActive) {
|
if (isShortCodeActive) {
|
||||||
this.cannedResponseSearchKey = value.substr(1, value.length);
|
this.cannedResponseSearchKey = value.substring(1);
|
||||||
this.showCannedResponseMenu = true;
|
this.showCannedResponseMenu = true;
|
||||||
} else {
|
} else {
|
||||||
this.cannedResponseSearchKey = '';
|
this.cannedResponseSearchKey = '';
|
||||||
|
|
|
@ -16,7 +16,12 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="fs-small">{{ category.name }}</span>
|
<router-link
|
||||||
|
class="fs-small button clear link secondary"
|
||||||
|
:to="getCategoryRoute(category.slug)"
|
||||||
|
>
|
||||||
|
{{ category.name }}
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="fs-small">
|
<span class="fs-small">
|
||||||
|
@ -43,6 +48,8 @@
|
||||||
<script>
|
<script>
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
import portalMixin from '../mixins/portalMixin';
|
import portalMixin from '../mixins/portalMixin';
|
||||||
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [timeMixin, portalMixin],
|
mixins: [timeMixin, portalMixin],
|
||||||
|
|
||||||
|
@ -97,6 +104,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
getCategoryRoute(categorySlug) {
|
||||||
|
const { portalSlug, locale } = this.$route.params;
|
||||||
|
return frontendURL(
|
||||||
|
`accounts/${this.accountId}/portals/${portalSlug}/${locale}/categories/${categorySlug}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,7 @@ import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
|
||||||
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
|
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
|
||||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
|
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
|
||||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel';
|
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel';
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import portalMixin from '../mixins/portalMixin';
|
import portalMixin from '../mixins/portalMixin';
|
||||||
import AddCategory from '../pages/categories/AddCategory';
|
import AddCategory from '../pages/categories/AddCategory';
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ export default {
|
||||||
PortalPopover,
|
PortalPopover,
|
||||||
AddCategory,
|
AddCategory,
|
||||||
},
|
},
|
||||||
mixins: [portalMixin],
|
mixins: [portalMixin, uiSettingsMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isSidebarOpen: false,
|
isSidebarOpen: false,
|
||||||
|
@ -231,7 +232,13 @@ export default {
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
const slug = this.$route.params.portalSlug;
|
const slug = this.$route.params.portalSlug;
|
||||||
if (slug) this.lastActivePortalSlug = slug;
|
if (slug) {
|
||||||
|
this.lastActivePortalSlug = slug;
|
||||||
|
this.updateUISettings({
|
||||||
|
last_active_portal_slug: slug,
|
||||||
|
last_active_locale_code: this.selectedLocaleInPortal,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleResize() {
|
handleResize() {
|
||||||
|
|
|
@ -188,13 +188,15 @@
|
||||||
<script>
|
<script>
|
||||||
import thumbnail from 'dashboard/components/widgets/Thumbnail';
|
import thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||||
import LocaleItemTable from './PortalListItemTable';
|
import LocaleItemTable from './PortalListItemTable';
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
thumbnail,
|
thumbnail,
|
||||||
LocaleItemTable,
|
LocaleItemTable,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
mixins: [alertMixin, uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
portal: {
|
portal: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -274,6 +276,10 @@ export default {
|
||||||
this.alertMessage = this.$t(
|
this.alertMessage = this.$t(
|
||||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS'
|
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS'
|
||||||
);
|
);
|
||||||
|
this.updateUISettings({
|
||||||
|
last_active_portal_slug: undefined,
|
||||||
|
last_active_locale_code: undefined,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertMessage =
|
this.alertMessage =
|
||||||
error?.message ||
|
error?.message ||
|
||||||
|
|
|
@ -21,12 +21,20 @@ const EditCategory = () => import('./pages/categories/EditCategory');
|
||||||
const ListCategoryArticles = () =>
|
const ListCategoryArticles = () =>
|
||||||
import('./pages/articles/ListCategoryArticles');
|
import('./pages/articles/ListCategoryArticles');
|
||||||
const ListAllArticles = () => import('./pages/articles/ListAllArticles');
|
const ListAllArticles = () => import('./pages/articles/ListAllArticles');
|
||||||
|
const DefaultPortalArticles = () =>
|
||||||
|
import('./pages/articles/DefaultPortalArticles');
|
||||||
const NewArticle = () => import('./pages/articles/NewArticle');
|
const NewArticle = () => import('./pages/articles/NewArticle');
|
||||||
const EditArticle = () => import('./pages/articles/EditArticle');
|
const EditArticle = () => import('./pages/articles/EditArticle');
|
||||||
|
|
||||||
const portalRoutes = [
|
const portalRoutes = [
|
||||||
{
|
{
|
||||||
path: getPortalRoute(''),
|
path: getPortalRoute(''),
|
||||||
|
name: 'default_portal_articles',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
component: DefaultPortalArticles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: getPortalRoute('all'),
|
||||||
name: 'list_all_portals',
|
name: 'list_all_portals',
|
||||||
roles: ['administrator', 'agent'],
|
roles: ['administrator', 'agent'],
|
||||||
component: ListAllPortals,
|
component: ListAllPortals,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [uiSettingsMixin],
|
||||||
|
mounted() {
|
||||||
|
const {
|
||||||
|
last_active_portal_slug: lastActivePortalSlug,
|
||||||
|
last_active_locale_code: lastActiveLocaleCode,
|
||||||
|
} = this.uiSettings || {};
|
||||||
|
|
||||||
|
if (lastActivePortalSlug)
|
||||||
|
this.$router.push({
|
||||||
|
name: 'list_all_locale_articles',
|
||||||
|
params: {
|
||||||
|
portalSlug: lastActivePortalSlug,
|
||||||
|
locale: lastActiveLocaleCode,
|
||||||
|
},
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
else
|
||||||
|
this.$router.push({
|
||||||
|
name: 'list_all_portals',
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -19,15 +19,11 @@
|
||||||
:script="currentInbox.callback_webhook_url"
|
:script="currentInbox.callback_webhook_url"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-6 small-offset-3">
|
<div v-if="isWhatsAppCloudInbox" class="medium-6 small-offset-3">
|
||||||
<p class="config--label">
|
<p class="config--label">
|
||||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}
|
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}
|
||||||
</p>
|
</p>
|
||||||
<woot-code
|
<woot-code lang="html" :script="currentInbox.callback_webhook_url" />
|
||||||
v-if="isWhatsAppCloudInbox"
|
|
||||||
lang="html"
|
|
||||||
:script="currentInbox.callback_webhook_url"
|
|
||||||
/>
|
|
||||||
<p class="config--label">
|
<p class="config--label">
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
|
@ -36,7 +32,6 @@
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<woot-code
|
<woot-code
|
||||||
v-if="isWhatsAppCloudInbox"
|
|
||||||
lang="html"
|
lang="html"
|
||||||
:script="currentInbox.provider_config.webhook_verify_token"
|
:script="currentInbox.provider_config.webhook_verify_token"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -243,7 +243,7 @@ export default {
|
||||||
this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', {
|
this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', {
|
||||||
options: JSON.stringify(options),
|
options: JSON.stringify(options),
|
||||||
}) +
|
}) +
|
||||||
script.substring(13, script.length)
|
script.substring(13)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getWidgetViewOptions() {
|
getWidgetViewOptions() {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
getUserCookieName,
|
getUserCookieName,
|
||||||
hasUserKeys,
|
hasUserKeys,
|
||||||
} from '../sdk/cookieHelpers';
|
} from '../sdk/cookieHelpers';
|
||||||
import { addClass, removeClass } from '../sdk/DOMHelpers';
|
import { addClasses, removeClasses } from '../sdk/DOMHelpers';
|
||||||
import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents';
|
import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents';
|
||||||
const runSDK = ({ baseUrl, websiteToken }) => {
|
const runSDK = ({ baseUrl, websiteToken }) => {
|
||||||
if (window.$chatwoot) {
|
if (window.$chatwoot) {
|
||||||
|
@ -41,12 +41,12 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||||
let widgetElm = document.querySelector('.woot--bubble-holder');
|
let widgetElm = document.querySelector('.woot--bubble-holder');
|
||||||
let widgetHolder = document.querySelector('.woot-widget-holder');
|
let widgetHolder = document.querySelector('.woot-widget-holder');
|
||||||
if (visibility === 'hide') {
|
if (visibility === 'hide') {
|
||||||
addClass(widgetHolder, 'woot-widget--without-bubble');
|
addClasses(widgetHolder, 'woot-widget--without-bubble');
|
||||||
addClass(widgetElm, 'woot-hidden');
|
addClasses(widgetElm, 'woot-hidden');
|
||||||
window.$chatwoot.hideMessageBubble = true;
|
window.$chatwoot.hideMessageBubble = true;
|
||||||
} else if (visibility === 'show') {
|
} else if (visibility === 'show') {
|
||||||
removeClass(widgetElm, 'woot-hidden');
|
removeClasses(widgetElm, 'woot-hidden');
|
||||||
removeClass(widgetHolder, 'woot-widget--without-bubble');
|
removeClasses(widgetHolder, 'woot-widget--without-bubble');
|
||||||
window.$chatwoot.hideMessageBubble = false;
|
window.$chatwoot.hideMessageBubble = false;
|
||||||
}
|
}
|
||||||
IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, {
|
IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, {
|
||||||
|
|
|
@ -3,68 +3,20 @@ import { IFrameHelper } from './IFrameHelper';
|
||||||
|
|
||||||
export const loadCSS = () => {
|
export const loadCSS = () => {
|
||||||
const css = document.createElement('style');
|
const css = document.createElement('style');
|
||||||
css.type = 'text/css';
|
|
||||||
css.innerHTML = `${SDK_CSS}`;
|
css.innerHTML = `${SDK_CSS}`;
|
||||||
document.body.appendChild(css);
|
document.body.appendChild(css);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const wootOn = (elm, event, fn) => {
|
export const addClasses = (elm, classes) => {
|
||||||
if (document.addEventListener) {
|
elm.classList.add(...classes.split(' '));
|
||||||
elm.addEventListener(event, fn, false);
|
|
||||||
} else if (document.attachEvent) {
|
|
||||||
// <= IE 8 loses scope so need to apply, we add this to object so we
|
|
||||||
// can detach later (can't detach anonymous functions)
|
|
||||||
// eslint-disable-next-line
|
|
||||||
elm[event + fn] = function() {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
return fn.apply(elm, arguments);
|
|
||||||
};
|
|
||||||
elm.attachEvent(`on${event}`, elm[event + fn]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const classHelper = (classes, action, elm) => {
|
|
||||||
let search;
|
|
||||||
let replace;
|
|
||||||
let i;
|
|
||||||
let has = false;
|
|
||||||
if (classes) {
|
|
||||||
// Trim any whitespace
|
|
||||||
const classarray = classes.split(/\s+/);
|
|
||||||
for (i = 0; i < classarray.length; i += 1) {
|
|
||||||
search = new RegExp(`\\b${classarray[i]}\\b`, 'g');
|
|
||||||
replace = new RegExp(` *${classarray[i]}\\b`, 'g');
|
|
||||||
if (action === 'remove') {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
elm.className = elm.className.replace(replace, '');
|
|
||||||
} else if (action === 'toggle') {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
elm.className = elm.className.match(search)
|
|
||||||
? elm.className.replace(replace, '')
|
|
||||||
: `${elm.className} ${classarray[i]}`;
|
|
||||||
} else if (action === 'has') {
|
|
||||||
if (elm.className.match(search)) {
|
|
||||||
has = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return has;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addClass = (elm, classes) => {
|
|
||||||
if (classes) {
|
|
||||||
elm.className += ` ${classes}`;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleClass = (elm, classes) => {
|
export const toggleClass = (elm, classes) => {
|
||||||
classHelper(classes, 'toggle', elm);
|
elm.classList.toggle(classes);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeClass = (elm, classes) => {
|
export const removeClasses = (elm, classes) => {
|
||||||
classHelper(classes, 'remove', elm);
|
elm.classList.remove(...classes.split(' '));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onLocationChange = ({ referrerURL, referrerHost }) => {
|
export const onLocationChange = ({ referrerURL, referrerHost }) => {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import {
|
import {
|
||||||
wootOn,
|
addClasses,
|
||||||
addClass,
|
|
||||||
loadCSS,
|
loadCSS,
|
||||||
removeClass,
|
removeClasses,
|
||||||
onLocationChangeListener,
|
onLocationChangeListener,
|
||||||
} from './DOMHelpers';
|
} from './DOMHelpers';
|
||||||
import {
|
import {
|
||||||
|
@ -68,7 +67,7 @@ export const IFrameHelper = {
|
||||||
holderClassName += ` woot-widget-holder--flat`;
|
holderClassName += ` woot-widget-holder--flat`;
|
||||||
}
|
}
|
||||||
|
|
||||||
addClass(widgetHolder, holderClassName);
|
addClasses(widgetHolder, holderClassName);
|
||||||
widgetHolder.appendChild(iframe);
|
widgetHolder.appendChild(iframe);
|
||||||
body.appendChild(widgetHolder);
|
body.appendChild(widgetHolder);
|
||||||
IFrameHelper.initPostMessageCommunication();
|
IFrameHelper.initPostMessageCommunication();
|
||||||
|
@ -99,7 +98,7 @@ export const IFrameHelper = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
initWindowSizeListener: () => {
|
initWindowSizeListener: () => {
|
||||||
wootOn(window, 'resize', () => IFrameHelper.toggleCloseButton());
|
window.addEventListener('resize', () => IFrameHelper.toggleCloseButton());
|
||||||
},
|
},
|
||||||
preventDefaultScroll: () => {
|
preventDefaultScroll: () => {
|
||||||
widgetHolder.addEventListener('wheel', event => {
|
widgetHolder.addEventListener('wheel', event => {
|
||||||
|
@ -241,9 +240,9 @@ export const IFrameHelper = {
|
||||||
event.unreadMessageCount > 0 &&
|
event.unreadMessageCount > 0 &&
|
||||||
!bubbleElement.classList.contains('unread-notification')
|
!bubbleElement.classList.contains('unread-notification')
|
||||||
) {
|
) {
|
||||||
addClass(bubbleElement, 'unread-notification');
|
addClasses(bubbleElement, 'unread-notification');
|
||||||
} else if (event.unreadMessageCount === 0) {
|
} else if (event.unreadMessageCount === 0) {
|
||||||
removeClass(bubbleElement, 'unread-notification');
|
removeClasses(bubbleElement, 'unread-notification');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -284,7 +283,7 @@ export const IFrameHelper = {
|
||||||
target: chatBubble,
|
target: chatBubble,
|
||||||
});
|
});
|
||||||
|
|
||||||
addClass(closeBubble, closeBtnClassName);
|
addClasses(closeBubble, closeBtnClassName);
|
||||||
|
|
||||||
chatIcon.style.background = widgetColor;
|
chatIcon.style.background = widgetColor;
|
||||||
closeBubble.style.background = widgetColor;
|
closeBubble.style.background = widgetColor;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers';
|
import { addClasses, removeClasses, toggleClass } from './DOMHelpers';
|
||||||
import { IFrameHelper } from './IFrameHelper';
|
import { IFrameHelper } from './IFrameHelper';
|
||||||
import { isExpandedView } from './settingsHelper';
|
import { isExpandedView } from './settingsHelper';
|
||||||
|
|
||||||
|
@ -41,14 +41,14 @@ export const createBubbleIcon = ({ className, src, target }) => {
|
||||||
|
|
||||||
export const createBubbleHolder = hideMessageBubble => {
|
export const createBubbleHolder = hideMessageBubble => {
|
||||||
if (hideMessageBubble) {
|
if (hideMessageBubble) {
|
||||||
addClass(bubbleHolder, 'woot-hidden');
|
addClasses(bubbleHolder, 'woot-hidden');
|
||||||
}
|
}
|
||||||
addClass(bubbleHolder, 'woot--bubble-holder');
|
addClasses(bubbleHolder, 'woot--bubble-holder');
|
||||||
body.appendChild(bubbleHolder);
|
body.appendChild(bubbleHolder);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createNotificationBubble = () => {
|
export const createNotificationBubble = () => {
|
||||||
addClass(notificationBubble, 'woot--notification');
|
addClasses(notificationBubble, 'woot--notification');
|
||||||
return notificationBubble;
|
return notificationBubble;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,15 +71,15 @@ export const onBubbleClick = (props = {}) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onClickChatBubble = () => {
|
export const onClickChatBubble = () => {
|
||||||
wootOn(bubbleHolder, 'click', onBubbleClick);
|
bubbleHolder.addEventListener('click', onBubbleClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addUnreadClass = () => {
|
export const addUnreadClass = () => {
|
||||||
const holderEl = document.querySelector('.woot-widget-holder');
|
const holderEl = document.querySelector('.woot-widget-holder');
|
||||||
addClass(holderEl, 'has-unread-view');
|
addClasses(holderEl, 'has-unread-view');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeUnreadClass = () => {
|
export const removeUnreadClass = () => {
|
||||||
const holderEl = document.querySelector('.woot-widget-holder');
|
const holderEl = document.querySelector('.woot-widget-holder');
|
||||||
removeClass(holderEl, 'has-unread-view');
|
removeClasses(holderEl, 'has-unread-view');
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,9 +41,18 @@
|
||||||
:class="{ 'dropdown-pane--open': showSearchDropdown }"
|
:class="{ 'dropdown-pane--open': showSearchDropdown }"
|
||||||
class="dropdown-pane"
|
class="dropdown-pane"
|
||||||
>
|
>
|
||||||
<h4 class="text-block-title text-truncate">
|
<div class="dropdown__header">
|
||||||
{{ multiselectorTitle }}
|
<h4 class="text-block-title text-truncate">
|
||||||
</h4>
|
{{ multiselectorTitle }}
|
||||||
|
</h4>
|
||||||
|
<woot-button
|
||||||
|
icon="dismiss"
|
||||||
|
size="tiny"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="clear"
|
||||||
|
@click="onCloseDropdown"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<multiselect-dropdown-items
|
<multiselect-dropdown-items
|
||||||
v-if="showSearchDropdown"
|
v-if="showSearchDropdown"
|
||||||
:options="options"
|
:options="options"
|
||||||
|
@ -172,4 +181,15 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-smaller);
|
||||||
|
|
||||||
|
.text-block-title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
<woot-button
|
<woot-button
|
||||||
class="multiselect-dropdown--item"
|
class="multiselect-dropdown--item"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
:class="{
|
:class="{
|
||||||
active: option.id === (selectedItem && selectedItem.id),
|
active: option.id === (selectedItem && selectedItem.id),
|
||||||
}"
|
}"
|
||||||
|
@ -129,7 +130,7 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: 16rem;
|
max-height: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-wrap {
|
.search-wrap {
|
||||||
|
@ -162,7 +163,7 @@ export default {
|
||||||
|
|
||||||
.multiselect-dropdown--list {
|
.multiselect-dropdown--list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 12rem;
|
max-height: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect-dropdown--item {
|
.multiselect-dropdown--item {
|
||||||
|
@ -170,13 +171,13 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--w-50);
|
background-color: var(--w-25);
|
||||||
color: var(--w-900);
|
color: var(--w-800);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,19 +29,16 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dropdown-menu__item {
|
.dropdown-menu__item {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
margin-bottom: var(--space-micro);
|
||||||
|
|
||||||
::v-deep {
|
::v-deep {
|
||||||
a,
|
a,
|
||||||
.button {
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
white-space: nowrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--s-700);
|
color: var(--s-700);
|
||||||
white-space: nowrap;
|
|
||||||
display: inline-flex;
|
|
||||||
padding: var(--space-small);
|
|
||||||
padding-top: var(--space-small);
|
|
||||||
padding-bottom: var(--space-small);
|
|
||||||
border-radius: var(--border-radius-normal);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { CAMPAIGN_TYPES } from '../constants/campaign';
|
import { CAMPAIGN_TYPES } from '../constants/campaign';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
campaignType() {
|
campaignType() {
|
||||||
const pageURL = window.location.href;
|
const pageURL = window.location.href;
|
||||||
const type = pageURL.substr(pageURL.lastIndexOf('/') + 1);
|
return pageURL.substring(pageURL.lastIndexOf('/') + 1);
|
||||||
return type;
|
|
||||||
},
|
},
|
||||||
isOngoingType() {
|
isOngoingType() {
|
||||||
return this.campaignType === CAMPAIGN_TYPES.ONGOING;
|
return this.campaignType === CAMPAIGN_TYPES.ONGOING;
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
surveyId() {
|
surveyId() {
|
||||||
const pageURL = window.location.href;
|
const pageURL = window.location.href;
|
||||||
return pageURL.substr(pageURL.lastIndexOf('/') + 1);
|
return pageURL.substring(pageURL.lastIndexOf('/') + 1);
|
||||||
},
|
},
|
||||||
isRatingSubmitted() {
|
isRatingSubmitted() {
|
||||||
return this.surveyDetails && this.surveyDetails.rating;
|
return this.surveyDetails && this.surveyDetails.rating;
|
||||||
|
|
|
@ -3,6 +3,5 @@ import { API } from 'widget/helpers/axios';
|
||||||
|
|
||||||
export const getAvailableAgents = async websiteToken => {
|
export const getAvailableAgents = async websiteToken => {
|
||||||
const urlData = endPoints.getAvailableAgents(websiteToken);
|
const urlData = endPoints.getAvailableAgents(websiteToken);
|
||||||
const result = await API.get(urlData.url, { params: urlData.params });
|
return API.get(urlData.url, { params: urlData.params });
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { API } from 'widget/helpers/axios';
|
||||||
|
|
||||||
const getCampaigns = async websiteToken => {
|
const getCampaigns = async websiteToken => {
|
||||||
const urlData = endPoints.getCampaigns(websiteToken);
|
const urlData = endPoints.getCampaigns(websiteToken);
|
||||||
const result = await API.get(urlData.url, { params: urlData.params });
|
return API.get(urlData.url, { params: urlData.params });
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerCampaign = async ({
|
const triggerCampaign = async ({
|
||||||
|
|
|
@ -3,26 +3,22 @@ import { API } from 'widget/helpers/axios';
|
||||||
|
|
||||||
const createConversationAPI = async content => {
|
const createConversationAPI = async content => {
|
||||||
const urlData = endPoints.createConversation(content);
|
const urlData = endPoints.createConversation(content);
|
||||||
const result = await API.post(urlData.url, urlData.params);
|
return API.post(urlData.url, urlData.params);
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessageAPI = async content => {
|
const sendMessageAPI = async content => {
|
||||||
const urlData = endPoints.sendMessage(content);
|
const urlData = endPoints.sendMessage(content);
|
||||||
const result = await API.post(urlData.url, urlData.params);
|
return API.post(urlData.url, urlData.params);
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendAttachmentAPI = async attachment => {
|
const sendAttachmentAPI = async attachment => {
|
||||||
const urlData = endPoints.sendAttachment(attachment);
|
const urlData = endPoints.sendAttachment(attachment);
|
||||||
const result = await API.post(urlData.url, urlData.params);
|
return API.post(urlData.url, urlData.params);
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMessagesAPI = async ({ before }) => {
|
const getMessagesAPI = async ({ before }) => {
|
||||||
const urlData = endPoints.getConversation({ before });
|
const urlData = endPoints.getConversation({ before });
|
||||||
const result = await API.get(urlData.url, { params: urlData.params });
|
return API.get(urlData.url, { params: urlData.params });
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConversationAPI = async () => {
|
const getConversationAPI = async () => {
|
||||||
|
|
|
@ -61,10 +61,7 @@ body {
|
||||||
|
|
||||||
.is-flat-design {
|
.is-flat-design {
|
||||||
.chat-bubble {
|
.chat-bubble {
|
||||||
border-bottom-left-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
border-bottom-right-radius: 0 !important;
|
|
||||||
border-top-left-radius: 0 !important;
|
|
||||||
border-top-right-radius: 0 !important;
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,8 +104,7 @@ export default {
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.message.content) return false;
|
return this.message.content;
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
readableTime() {
|
readableTime() {
|
||||||
const { created_at: createdAt = '' } = this.message;
|
const { created_at: createdAt = '' } = this.message;
|
||||||
|
|
|
@ -54,9 +54,9 @@ export default {
|
||||||
},
|
},
|
||||||
async onFileUpload(file) {
|
async onFileUpload(file) {
|
||||||
if (this.globalConfig.directUploadsEnabled) {
|
if (this.globalConfig.directUploadsEnabled) {
|
||||||
this.onDirectFileUpload(file);
|
await this.onDirectFileUpload(file);
|
||||||
} else {
|
} else {
|
||||||
this.onIndirectFileUpload(file);
|
await this.onIndirectFileUpload(file);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onDirectFileUpload(file) {
|
async onDirectFileUpload(file) {
|
||||||
|
|
|
@ -7,7 +7,12 @@
|
||||||
class="flex items-start"
|
class="flex items-start"
|
||||||
:class="[avatarUrl ? 'justify-between' : 'justify-end']"
|
:class="[avatarUrl ? 'justify-between' : 'justify-end']"
|
||||||
>
|
>
|
||||||
<img v-if="avatarUrl" class="h-12 rounded-full" :src="avatarUrl" />
|
<img
|
||||||
|
v-if="avatarUrl"
|
||||||
|
class="h-12 rounded-full"
|
||||||
|
:src="avatarUrl"
|
||||||
|
alt="Avatar"
|
||||||
|
/>
|
||||||
<header-actions :show-popout-button="showPopoutButton" />
|
<header-actions :show-popout-button="showPopoutButton" />
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
|
|
|
@ -209,9 +209,7 @@ export default {
|
||||||
min-height: $space-large;
|
min-height: $space-large;
|
||||||
max-height: 2.4 * $space-mega;
|
max-height: 2.4 * $space-mega;
|
||||||
resize: none;
|
resize: none;
|
||||||
padding: 0;
|
padding: $space-smaller 0;
|
||||||
padding-top: $space-smaller;
|
|
||||||
padding-bottom: $space-smaller;
|
|
||||||
margin-top: $space-small;
|
margin-top: $space-small;
|
||||||
margin-bottom: $space-small;
|
margin-bottom: $space-small;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,7 @@ export default {
|
||||||
: decodeURI(this.fileName);
|
: decodeURI(this.fileName);
|
||||||
},
|
},
|
||||||
fileName() {
|
fileName() {
|
||||||
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
return this.url.substring(this.url.lastIndexOf('/') + 1);
|
||||||
return filename;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -118,13 +118,10 @@ export default {
|
||||||
const isUserEmailAvailable = !!this.currentUser.email;
|
const isUserEmailAvailable = !!this.currentUser.email;
|
||||||
const isUserPhoneNumberAvailable = !!this.currentUser.phone_number;
|
const isUserPhoneNumberAvailable = !!this.currentUser.phone_number;
|
||||||
return this.preChatFields.filter(field => {
|
return this.preChatFields.filter(field => {
|
||||||
if (
|
return !(
|
||||||
(isUserEmailAvailable && field.name === 'emailAddress') ||
|
(isUserEmailAvailable && field.name === 'emailAddress') ||
|
||||||
(isUserPhoneNumberAvailable && field.name === 'phoneNumber')
|
(isUserPhoneNumberAvailable && field.name === 'phoneNumber')
|
||||||
) {
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enabledPreChatFields() {
|
enabledPreChatFields() {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs leading-4 mt-1">
|
<div class="text-xs leading-4 mt-1">
|
||||||
{{ replyWaitMeessage }}
|
{{ replyWaitMessage }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<available-agents v-if="isOnline" :agents="availableAgents" />
|
<available-agents v-if="isOnline" :agents="availableAgents" />
|
||||||
|
@ -75,7 +75,7 @@ export default {
|
||||||
}
|
}
|
||||||
return anyAgentOnline;
|
return anyAgentOnline;
|
||||||
},
|
},
|
||||||
replyWaitMeessage() {
|
replyWaitMessage() {
|
||||||
const { workingHoursEnabled } = this.channelConfig;
|
const { workingHoursEnabled } = this.channelConfig;
|
||||||
|
|
||||||
if (this.isOnline) {
|
if (this.isOnline) {
|
||||||
|
|
|
@ -107,13 +107,12 @@ export default {
|
||||||
.clear-button {
|
.clear-button {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $color-woot;
|
color: $color-woot;
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
font-size: $font-size-medium;
|
font-size: $font-size-medium;
|
||||||
transition: all 0.3s var(--ease-in-cubic);
|
transition: all 0.3s var(--ease-in-cubic);
|
||||||
margin-left: $space-smaller;
|
margin-left: $space-smaller;
|
||||||
padding-right: $space-one;
|
padding: 0 $space-one 0 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateX($space-smaller);
|
transform: translateX($space-smaller);
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const loadedEventConfig = () => {
|
||||||
|
|
||||||
export const getExtraSpaceToScroll = () => {
|
export const getExtraSpaceToScroll = () => {
|
||||||
// This function calculates the extra space needed for the view to
|
// This function calculates the extra space needed for the view to
|
||||||
// accomodate the height of close button + height of
|
// accommodate the height of close button + height of
|
||||||
// read messages button. So that scrollbar won't appear
|
// read messages button. So that scrollbar won't appear
|
||||||
const unreadMessageWrap = document.querySelector('.unread-messages');
|
const unreadMessageWrap = document.querySelector('.unread-messages');
|
||||||
const unreadCloseWrap = document.querySelector('.close-unread-wrap');
|
const unreadCloseWrap = document.querySelector('.close-unread-wrap');
|
||||||
|
|
|
@ -3,13 +3,6 @@ import { WOOT_PREFIX } from './constants';
|
||||||
export const isEmptyObject = obj =>
|
export const isEmptyObject = obj =>
|
||||||
Object.keys(obj).length === 0 && obj.constructor === Object;
|
Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||||
|
|
||||||
export const arrayToHashById = array =>
|
|
||||||
array.reduce((map, obj) => {
|
|
||||||
const newMap = map;
|
|
||||||
newMap[obj.id] = obj;
|
|
||||||
return newMap;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
export const sendMessage = msg => {
|
export const sendMessage = msg => {
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
`chatwoot-widget:${JSON.stringify({ ...msg })}`,
|
`chatwoot-widget:${JSON.stringify({ ...msg })}`,
|
||||||
|
@ -22,9 +15,7 @@ export const IFrameHelper = {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
isAValidEvent: e => {
|
isAValidEvent: e => {
|
||||||
const isDataAString = typeof e.data === 'string';
|
const isDataAString = typeof e.data === 'string';
|
||||||
const isAValidWootEvent =
|
return isDataAString && e.data.indexOf(WOOT_PREFIX) === 0;
|
||||||
isDataAString && e.data.indexOf(WOOT_PREFIX) === 0;
|
|
||||||
return isAValidWootEvent;
|
|
||||||
},
|
},
|
||||||
getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')),
|
getMessage: e => JSON.parse(e.data.replace(WOOT_PREFIX, '')),
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const getters = {
|
||||||
},
|
},
|
||||||
getUnreadMessageCount: _state => {
|
getUnreadMessageCount: _state => {
|
||||||
const { userLastSeenAt } = _state.meta;
|
const { userLastSeenAt } = _state.meta;
|
||||||
const count = Object.values(_state.conversations).filter(chat => {
|
return Object.values(_state.conversations).filter(chat => {
|
||||||
const { created_at: createdAt, message_type: messageType } = chat;
|
const { created_at: createdAt, message_type: messageType } = chat;
|
||||||
const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING;
|
const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
const hasNotSeen = userLastSeenAt
|
const hasNotSeen = userLastSeenAt
|
||||||
|
@ -40,7 +40,6 @@ export const getters = {
|
||||||
: true;
|
: true;
|
||||||
return hasNotSeen && isOutGoing;
|
return hasNotSeen && isOutGoing;
|
||||||
}).length;
|
}).length;
|
||||||
return count;
|
|
||||||
},
|
},
|
||||||
getUnreadTextMessages: (_state, _getters) => {
|
getUnreadTextMessages: (_state, _getters) => {
|
||||||
const unreadCount = _getters.getUnreadMessageCount;
|
const unreadCount = _getters.getUnreadMessageCount;
|
||||||
|
@ -50,7 +49,6 @@ export const getters = {
|
||||||
return messageType === MESSAGE_TYPE.OUTGOING;
|
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
});
|
});
|
||||||
const maxUnreadCount = Math.min(unreadCount, 3);
|
const maxUnreadCount = Math.min(unreadCount, 3);
|
||||||
const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount);
|
return unreadAgentMessages.splice(-maxUnreadCount);
|
||||||
return allUnreadMessages;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@ const shouldShowAvatar = (message, nextMessage) => {
|
||||||
|
|
||||||
export const groupConversationBySender = conversationsForADate =>
|
export const groupConversationBySender = conversationsForADate =>
|
||||||
conversationsForADate.map((message, index) => {
|
conversationsForADate.map((message, index) => {
|
||||||
let showAvatar = false;
|
let showAvatar;
|
||||||
const isLastMessage = index === conversationsForADate.length - 1;
|
const isLastMessage = index === conversationsForADate.length - 1;
|
||||||
if (isASubmittedFormMessage(message)) {
|
if (isASubmittedFormMessage(message)) {
|
||||||
showAvatar = false;
|
showAvatar = false;
|
||||||
|
|
|
@ -88,8 +88,7 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleAgentTypingStatus($state, { status }) {
|
toggleAgentTypingStatus($state, { status }) {
|
||||||
const isTyping = status === 'on';
|
$state.uiFlags.isAgentTyping = status === 'on';
|
||||||
$state.uiFlags.isAgentTyping = isTyping;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setMetaUserLastSeenAt($state, lastSeen) {
|
setMetaUserLastSeenAt($state, lastSeen) {
|
||||||
|
|
|
@ -9,14 +9,14 @@ export const actions = {
|
||||||
try {
|
try {
|
||||||
await conversationLabels.create(label);
|
await conversationLabels.create(label);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ingore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroy: async (_, label) => {
|
destroy: async (_, label) => {
|
||||||
try {
|
try {
|
||||||
await conversationLabels.destroy(label);
|
await conversationLabels.destroy(label);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ingore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,7 +34,7 @@ module MailboxHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact
|
def create_contact
|
||||||
@contact_inbox = ::ContactBuilder.new(
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: processed_mail.original_sender,
|
source_id: processed_mail.original_sender,
|
||||||
inbox: @inbox,
|
inbox: @inbox,
|
||||||
contact_attributes: {
|
contact_attributes: {
|
||||||
|
|
|
@ -37,16 +37,11 @@ class Channel::FacebookPage < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(instagram_id, name)
|
def create_contact_inbox(instagram_id, name)
|
||||||
ActiveRecord::Base.transaction do
|
@contact_inbox = ::ContactInboxWithContactBuilder.new({
|
||||||
contact = inbox.account.contacts.create!(name: name)
|
source_id: instagram_id,
|
||||||
::ContactInbox.create!(
|
inbox: inbox,
|
||||||
contact_id: contact.id,
|
contact_attributes: { name: name }
|
||||||
inbox_id: inbox.id,
|
}).perform
|
||||||
source_id: instagram_id
|
|
||||||
)
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribe
|
def subscribe
|
||||||
|
|
|
@ -32,16 +32,11 @@ class Channel::TwitterProfile < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(profile_id, name, additional_attributes)
|
def create_contact_inbox(profile_id, name, additional_attributes)
|
||||||
ActiveRecord::Base.transaction do
|
::ContactInboxWithContactBuilder.new({
|
||||||
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name)
|
source_id: profile_id,
|
||||||
::ContactInbox.create!(
|
inbox: inbox,
|
||||||
contact_id: contact.id,
|
contact_attributes: { name: name, additional_attributes: additional_attributes }
|
||||||
inbox_id: inbox.id,
|
}).perform
|
||||||
source_id: profile_id
|
|
||||||
)
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def twitter_client
|
def twitter_client
|
||||||
|
|
|
@ -98,19 +98,9 @@ class Channel::WebWidget < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(additional_attributes = {})
|
def create_contact_inbox(additional_attributes = {})
|
||||||
ActiveRecord::Base.transaction do
|
::ContactInboxWithContactBuilder.new({
|
||||||
contact = inbox.account.contacts.create!(
|
inbox: inbox,
|
||||||
name: ::Haikunator.haikunate(1000),
|
contact_attributes: { additional_attributes: additional_attributes }
|
||||||
additional_attributes: additional_attributes
|
}).perform
|
||||||
)
|
|
||||||
contact_inbox = ::ContactInbox.create!(
|
|
||||||
contact_id: contact.id,
|
|
||||||
inbox_id: inbox.id,
|
|
||||||
source_id: SecureRandom.uuid
|
|
||||||
)
|
|
||||||
contact_inbox
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,10 @@ class WorkingHour < ApplicationRecord
|
||||||
validate :open_all_day_and_closed_all_day
|
validate :open_all_day_and_closed_all_day
|
||||||
|
|
||||||
def self.today
|
def self.today
|
||||||
find_by(day_of_week: Date.current.wday)
|
# While getting the day of the week, consider the timezone as well. `first` would
|
||||||
|
# return the first working hour from the list of working hours available per week.
|
||||||
|
inbox = first.inbox
|
||||||
|
find_by(day_of_week: Time.zone.now.in_time_zone(inbox.timezone).to_date.wday)
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_at?(time)
|
def open_at?(time)
|
||||||
|
|
|
@ -81,7 +81,7 @@ class Line::IncomingMessageService
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_contact
|
def set_contact
|
||||||
contact_inbox = ::ContactBuilder.new(
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: line_contact_info['userId'],
|
source_id: line_contact_info['userId'],
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
contact_attributes: contact_attributes
|
contact_attributes: contact_attributes
|
||||||
|
|
|
@ -37,7 +37,7 @@ class Sms::IncomingMessageService
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_contact
|
def set_contact
|
||||||
contact_inbox = ::ContactBuilder.new(
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: params[:from],
|
source_id: params[:from],
|
||||||
inbox: @inbox,
|
inbox: @inbox,
|
||||||
contact_attributes: contact_attributes
|
contact_attributes: contact_attributes
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Telegram::IncomingMessageService
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_contact
|
def set_contact
|
||||||
contact_inbox = ::ContactBuilder.new(
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: params[:message][:from][:id],
|
source_id: params[:message][:from][:id],
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
contact_attributes: contact_attributes
|
contact_attributes: contact_attributes
|
||||||
|
|
|
@ -47,7 +47,7 @@ class Twilio::IncomingMessageService
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_contact
|
def set_contact
|
||||||
contact_inbox = ::ContactBuilder.new(
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: params[:From],
|
source_id: params[:From],
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
contact_attributes: contact_attributes
|
contact_attributes: contact_attributes
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||||
|
|
||||||
set_conversation
|
set_conversation
|
||||||
|
|
||||||
return if @processed_params[:messages].blank?
|
return if @processed_params[:messages].blank? || unprocessable_message_type?
|
||||||
|
|
||||||
@message = @conversation.messages.build(
|
@message = @conversation.messages.build(
|
||||||
content: message_content(@processed_params[:messages].first),
|
content: message_content(@processed_params[:messages].first),
|
||||||
|
@ -48,7 +48,7 @@ class Whatsapp::IncomingMessageBaseService
|
||||||
contact_params = @processed_params[:contacts]&.first
|
contact_params = @processed_params[:contacts]&.first
|
||||||
return if contact_params.blank?
|
return if contact_params.blank?
|
||||||
|
|
||||||
contact_inbox = ::ContactBuilder.new(
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: contact_params[:wa_id],
|
source_id: contact_params[:wa_id],
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" }
|
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" }
|
||||||
|
@ -86,6 +86,10 @@ class Whatsapp::IncomingMessageBaseService
|
||||||
@processed_params[:messages].first[:type]
|
@processed_params[:messages].first[:type]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unprocessable_message_type?
|
||||||
|
%w[reaction contacts].include?(message_type)
|
||||||
|
end
|
||||||
|
|
||||||
def attach_files
|
def attach_files
|
||||||
return if %w[text button interactive].include?(message_type)
|
return if %w[text button interactive].include?(message_type)
|
||||||
|
|
||||||
|
|
|
@ -46,8 +46,13 @@ unless Rails.env.production?
|
||||||
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
|
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
|
||||||
InboxMember.create!(user: user, inbox: inbox)
|
InboxMember.create!(user: user, inbox: inbox)
|
||||||
|
|
||||||
contact = Contact.create!(name: 'jane', email: 'jane@example.com', phone_number: '+2320000', account: account)
|
contact = ::ContactInboxWithContactBuilder.new(
|
||||||
contact_inbox = ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id, hmac_verified: true)
|
source_id: user.id,
|
||||||
|
inbox: inbox,
|
||||||
|
hmac_verified: true,
|
||||||
|
contact_attributes: { name: 'jane', email: 'jane@example.com', phone_number: '+2320000' }
|
||||||
|
).perform&.contact
|
||||||
|
|
||||||
conversation = Conversation.create!(
|
conversation = Conversation.create!(
|
||||||
account: account,
|
account: account,
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
|
|
|
@ -12,8 +12,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id,
|
inbox: twilio_inbox,
|
||||||
source_id: contact.phone_number
|
source_id: contact.phone_number
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id
|
inbox: twilio_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
|
@ -33,8 +33,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id,
|
inbox: twilio_inbox,
|
||||||
source_id: '+224213223422'
|
source_id: '+224213223422'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -44,12 +44,23 @@ describe ::ContactInboxBuilder do
|
||||||
|
|
||||||
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id
|
inbox: twilio_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).to eq(contact.phone_number)
|
expect(contact_inbox.source_id).to eq(contact.phone_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'raises error when contact phone number is not present and no source id is provided' do
|
||||||
|
contact.update!(phone_number: nil)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new(
|
||||||
|
contact: contact,
|
||||||
|
inbox: twilio_inbox
|
||||||
|
).perform
|
||||||
|
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'twilio whatsapp inbox' do
|
describe 'twilio whatsapp inbox' do
|
||||||
|
@ -59,8 +70,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id,
|
inbox: twilio_inbox,
|
||||||
source_id: "whatsapp:#{contact.phone_number}"
|
source_id: "whatsapp:#{contact.phone_number}"
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -70,8 +81,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id
|
inbox: twilio_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
|
@ -80,8 +91,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id,
|
inbox: twilio_inbox,
|
||||||
source_id: 'whatsapp:+555555'
|
source_id: 'whatsapp:+555555'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -91,12 +102,23 @@ describe ::ContactInboxBuilder do
|
||||||
|
|
||||||
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: twilio_inbox.id
|
inbox: twilio_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}")
|
expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'raises error when contact phone number is not present and no source id is provided' do
|
||||||
|
contact.update!(phone_number: nil)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new(
|
||||||
|
contact: contact,
|
||||||
|
inbox: twilio_inbox
|
||||||
|
).perform
|
||||||
|
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'whatsapp inbox' do
|
describe 'whatsapp inbox' do
|
||||||
|
@ -105,8 +127,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: whatsapp_inbox.id,
|
inbox: whatsapp_inbox,
|
||||||
source_id: contact.phone_number&.delete('+')
|
source_id: contact.phone_number&.delete('+')
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -116,8 +138,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: whatsapp_inbox.id
|
inbox: whatsapp_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
expect(contact_inbox.id).to be(existing_contact_inbox.id)
|
||||||
|
@ -126,8 +148,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: whatsapp_inbox.id,
|
inbox: whatsapp_inbox,
|
||||||
source_id: '555555'
|
source_id: '555555'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -137,12 +159,23 @@ describe ::ContactInboxBuilder do
|
||||||
|
|
||||||
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: whatsapp_inbox.id
|
inbox: whatsapp_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+'))
|
expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'raises error when contact phone number is not present and no source id is provided' do
|
||||||
|
contact.update!(phone_number: nil)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new(
|
||||||
|
contact: contact,
|
||||||
|
inbox: whatsapp_inbox
|
||||||
|
).perform
|
||||||
|
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'sms inbox' do
|
describe 'sms inbox' do
|
||||||
|
@ -152,8 +185,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: sms_inbox.id,
|
inbox: sms_inbox,
|
||||||
source_id: contact.phone_number
|
source_id: contact.phone_number
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -163,8 +196,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: sms_inbox.id
|
inbox: sms_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
|
@ -173,8 +206,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: sms_inbox.id,
|
inbox: sms_inbox,
|
||||||
source_id: '+224213223422'
|
source_id: '+224213223422'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -184,12 +217,23 @@ describe ::ContactInboxBuilder do
|
||||||
|
|
||||||
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: sms_inbox.id
|
inbox: sms_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).to eq(contact.phone_number)
|
expect(contact_inbox.source_id).to eq(contact.phone_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'raises error when contact phone number is not present and no source id is provided' do
|
||||||
|
contact.update!(phone_number: nil)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new(
|
||||||
|
contact: contact,
|
||||||
|
inbox: sms_inbox
|
||||||
|
).perform
|
||||||
|
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'email inbox' do
|
describe 'email inbox' do
|
||||||
|
@ -199,8 +243,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: email_inbox.id,
|
inbox: email_inbox,
|
||||||
source_id: contact.email
|
source_id: contact.email
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -210,8 +254,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do
|
it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: email_inbox.id
|
inbox: email_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
expect(contact_inbox.id).to eq(existing_contact_inbox.id)
|
||||||
|
@ -220,8 +264,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: email_inbox.id,
|
inbox: email_inbox,
|
||||||
source_id: 'xyc@xyc.com'
|
source_id: 'xyc@xyc.com'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -231,12 +275,23 @@ describe ::ContactInboxBuilder do
|
||||||
|
|
||||||
it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: email_inbox.id
|
inbox: email_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).to eq(contact.email)
|
expect(contact_inbox.source_id).to eq(contact.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'raises error when contact email is not present and no source id is provided' do
|
||||||
|
contact.update!(email: nil)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.new(
|
||||||
|
contact: contact,
|
||||||
|
inbox: email_inbox
|
||||||
|
).perform
|
||||||
|
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact email')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'api inbox' do
|
describe 'api inbox' do
|
||||||
|
@ -246,8 +301,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test')
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test')
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: api_inbox.id,
|
inbox: api_inbox,
|
||||||
source_id: 'test'
|
source_id: 'test'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -257,8 +312,8 @@ describe ::ContactInboxBuilder do
|
||||||
it 'creates a new contact inbox when different source id is provided' do
|
it 'creates a new contact inbox when different source id is provided' do
|
||||||
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid)
|
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid)
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: api_inbox.id,
|
inbox: api_inbox,
|
||||||
source_id: 'test'
|
source_id: 'test'
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
@ -268,61 +323,12 @@ describe ::ContactInboxBuilder do
|
||||||
|
|
||||||
it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do
|
it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do
|
||||||
contact_inbox = described_class.new(
|
contact_inbox = described_class.new(
|
||||||
contact_id: contact.id,
|
contact: contact,
|
||||||
inbox_id: api_inbox.id
|
inbox: api_inbox
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
expect(contact_inbox.source_id).not_to be_nil
|
expect(contact_inbox.source_id).not_to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'web widget' do
|
|
||||||
let!(:website_channel) { create(:channel_widget, account: account) }
|
|
||||||
let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) }
|
|
||||||
|
|
||||||
it 'does not create contact inbox' do
|
|
||||||
contact_inbox = described_class.new(
|
|
||||||
contact_id: contact.id,
|
|
||||||
inbox_id: website_inbox.id,
|
|
||||||
source_id: 'test'
|
|
||||||
).perform
|
|
||||||
|
|
||||||
expect(contact_inbox).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'facebook inbox' do
|
|
||||||
before do
|
|
||||||
stub_request(:post, /graph.facebook.com/)
|
|
||||||
end
|
|
||||||
|
|
||||||
let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
|
|
||||||
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
|
|
||||||
|
|
||||||
it 'does not create contact inbox' do
|
|
||||||
contact_inbox = described_class.new(
|
|
||||||
contact_id: contact.id,
|
|
||||||
inbox_id: facebook_inbox.id,
|
|
||||||
source_id: 'test'
|
|
||||||
).perform
|
|
||||||
|
|
||||||
expect(contact_inbox).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'twitter inbox' do
|
|
||||||
let!(:twitter_channel) { create(:channel_twitter_profile, account: account) }
|
|
||||||
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
|
|
||||||
|
|
||||||
it 'does not create contact inbox' do
|
|
||||||
contact_inbox = described_class.new(
|
|
||||||
contact_id: contact.id,
|
|
||||||
inbox_id: twitter_inbox.id,
|
|
||||||
source_id: 'test'
|
|
||||||
).perform
|
|
||||||
|
|
||||||
expect(contact_inbox).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe ::ContactBuilder do
|
describe ::ContactInboxWithContactBuilder do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let(:inbox) { create(:inbox, account: account) }
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') }
|
let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') }
|
|
@ -62,6 +62,59 @@ RSpec.describe Webhooks::WhatsappEventsJob, type: :job do
|
||||||
job.perform_now(wb_params)
|
job.perform_now(wb_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'Ignore reaction type message and stop raising error' do
|
||||||
|
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||||
|
validate_provider_config: false)
|
||||||
|
wb_params = {
|
||||||
|
phone_number: channel.phone_number,
|
||||||
|
object: 'whatsapp_business_account',
|
||||||
|
entry: [{
|
||||||
|
changes: [{
|
||||||
|
value: {
|
||||||
|
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
|
||||||
|
messages: [{
|
||||||
|
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
|
||||||
|
}],
|
||||||
|
metadata: {
|
||||||
|
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||||
|
display_phone_number: other_channel.phone_number.delete('+')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}.with_indifferent_access
|
||||||
|
expect do
|
||||||
|
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'Ignore contacts type message and stop raising error' do
|
||||||
|
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||||
|
validate_provider_config: false)
|
||||||
|
wb_params = {
|
||||||
|
phone_number: channel.phone_number,
|
||||||
|
object: 'whatsapp_business_account',
|
||||||
|
entry: [{
|
||||||
|
changes: [{
|
||||||
|
value: {
|
||||||
|
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
|
||||||
|
messages: [{ from: '1111981136571',
|
||||||
|
contacts: [{ phones: [{ phone: '+1987654' }], name: { first_name: 'contact name' } }],
|
||||||
|
timestamp: '1664799904',
|
||||||
|
type: 'contacts' }],
|
||||||
|
metadata: {
|
||||||
|
phone_number_id: other_channel.provider_config['phone_number_id'],
|
||||||
|
display_phone_number: other_channel.phone_number.delete('+')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}.with_indifferent_access
|
||||||
|
expect do
|
||||||
|
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
|
||||||
it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do
|
it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do
|
||||||
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
|
||||||
validate_provider_config: false)
|
validate_provider_config: false)
|
||||||
|
|
|
@ -88,4 +88,18 @@ RSpec.describe WorkingHour do
|
||||||
'Validation failed: open_all_day and closed_all_day cannot be true at the same time')
|
'Validation failed: open_all_day and closed_all_day cannot be true at the same time')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when on monday 9am in Sydney timezone' do
|
||||||
|
let(:inbox) { create(:inbox) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Time.zone = 'Australia/Sydney'
|
||||||
|
inbox.update(timezone: 'Australia/Sydney')
|
||||||
|
travel_to '10.10.2022 9:00 AEDT'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is considered working hour' do
|
||||||
|
expect(described_class.today.open_now?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -167,6 +167,7 @@ describe ::Contacts::FilterService do
|
||||||
|
|
||||||
context 'with x_days_before filter' do
|
context 'with x_days_before filter' do
|
||||||
before do
|
before do
|
||||||
|
Time.zone = 'UTC'
|
||||||
el_contact.update(last_activity_at: (Time.zone.today - 4.days))
|
el_contact.update(last_activity_at: (Time.zone.today - 4.days))
|
||||||
cs_contact.update(last_activity_at: (Time.zone.today - 5.days))
|
cs_contact.update(last_activity_at: (Time.zone.today - 5.days))
|
||||||
en_contact.update(last_activity_at: (Time.zone.today - 2.days))
|
en_contact.update(last_activity_at: (Time.zone.today - 2.days))
|
||||||
|
|
|
@ -309,6 +309,7 @@ describe ::Conversations::FilterService do
|
||||||
|
|
||||||
context 'with x_days_before filter' do
|
context 'with x_days_before filter' do
|
||||||
before do
|
before do
|
||||||
|
Time.zone = 'UTC'
|
||||||
en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days))
|
en_conversation_1.update!(last_activity_at: (Time.zone.today - 4.days))
|
||||||
en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days))
|
en_conversation_2.update!(last_activity_at: (Time.zone.today - 5.days))
|
||||||
user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days))
|
user_2_assigned_conversation.update!(last_activity_at: (Time.zone.today - 2.days))
|
||||||
|
|
Loading…
Reference in a new issue