Merge branch 'develop' into feat/add_lograge
This commit is contained in:
commit
8de0293ab5
595 changed files with 7617 additions and 2018 deletions
|
@ -54,3 +54,5 @@ exclude_patterns:
|
|||
- 'app/javascript/widget/i18n/index.js'
|
||||
- 'app/javascript/survey/i18n/index.js'
|
||||
- 'app/javascript/shared/constants/locales.js'
|
||||
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
|
||||
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'
|
||||
|
|
|
@ -7,8 +7,8 @@ end_of_line = lf
|
|||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = spaces
|
||||
indent_style = space
|
||||
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
|
||||
|
|
|
@ -34,6 +34,11 @@ REDIS_SENTINELS=
|
|||
# You can find list of master using "SENTINEL masters" command
|
||||
REDIS_SENTINEL_MASTER_NAME=
|
||||
|
||||
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
|
||||
# Use the following environment variable to customize passwords for sentinels.
|
||||
# Use empty string if sentinels are configured with out passwords
|
||||
# REDIS_SENTINEL_PASSWORD=
|
||||
|
||||
# Redis premium breakage in heroku fix
|
||||
# enable the following configuration
|
||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||
|
|
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -6,6 +6,7 @@ labels: 'Bug'
|
|||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
@ -16,11 +17,11 @@ Steps to reproduce the behavior:
|
|||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
4. See the error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
Share a clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
|
@ -28,27 +29,50 @@ If applicable, add screenshots to help explain your problem.
|
|||
|
||||
**Browser logs**
|
||||
|
||||
Share the browser logs to debug the issue further
|
||||
Share the browser logs to debug the issue further.
|
||||
|
||||
**Server logs**
|
||||
|
||||
Share the server logs to debug the issue further
|
||||
Share the server logs to debug the issue further.
|
||||
|
||||
**Environment**
|
||||
|
||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku)
|
||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- [ ] app.chatwoot.com (Chatwoot Cloud)
|
||||
- [ ] Self-hosted
|
||||
- - [ ] Linux VM
|
||||
- - [ ] Docker
|
||||
- - [ ] Kubernetes
|
||||
- - [ ] Heroku
|
||||
- - [ ] Other (Please specify)
|
||||
|
||||
|
||||
**Desktop (please complete the following information)** (If applicable)
|
||||
- OS: [e.g. Linux, Windows, MacOS]
|
||||
- Browser [e.g. chrome, firefox, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
**Smartphone (please complete the following information)** (If applicable)
|
||||
- Device: [e.g. iPhone6, Pixel7]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Browser [e.g. stock browser, firefox, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Docker** (If applicable)
|
||||
|
||||
Please share the output of the following.
|
||||
- `docker version`
|
||||
- `docker info`
|
||||
- `docker-compose version`
|
||||
|
||||
**Cloud Provider** (If applicable)
|
||||
- [ ] AWS
|
||||
- [ ] GCP
|
||||
- [ ] Azure
|
||||
- [ ] DigitalOcean
|
||||
- [ ] Others
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -2,8 +2,7 @@
|
|||
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
@ -12,18 +11,18 @@ Please delete options that are not relevant.
|
|||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
|
||||
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
|
|
2
.github/workflows/publish_foss_docker.yml
vendored
2
.github/workflows/publish_foss_docker.yml
vendored
|
@ -58,6 +58,6 @@ jobs:
|
|||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_TAG }}
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -39,9 +39,6 @@ public/packs*
|
|||
*.un~
|
||||
.jest-cache
|
||||
|
||||
#VS Code files
|
||||
.vscode
|
||||
|
||||
# ignore jetbrains IDE files
|
||||
.idea
|
||||
|
||||
|
@ -62,4 +59,4 @@ package-lock.json
|
|||
test/cypress/videos/*
|
||||
|
||||
/config/master.key
|
||||
/config/*.enc
|
||||
/config/*.enc
|
||||
|
|
|
@ -16,6 +16,7 @@ Metrics/ClassLength:
|
|||
- 'app/models/message.rb'
|
||||
- 'app/builders/messages/facebook/message_builder.rb'
|
||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
|
||||
- 'app/listeners/action_cable_listener.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
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",
|
||||
]
|
||||
}
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -432,14 +432,14 @@ GEM
|
|||
netrc (0.11.0)
|
||||
newrelic_rpm (8.9.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.7)
|
||||
nokogiri (1.13.9)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.7-arm64-darwin)
|
||||
nokogiri (1.13.9-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.7-x86_64-darwin)
|
||||
nokogiri (1.13.9-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.7-x86_64-linux)
|
||||
nokogiri (1.13.9-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.10)
|
||||
orm_adapter (0.5.0)
|
||||
|
@ -816,4 +816,4 @@ RUBY VERSION
|
|||
ruby 3.0.4p208
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.18
|
||||
2.3.16
|
||||
|
|
|
@ -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
|
||||
pattr_initialize [:contact_id!, :inbox_id!, :source_id]
|
||||
pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
|
||||
|
||||
def perform
|
||||
@contact = Contact.find(contact_id)
|
||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
||||
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?
|
||||
@source_id ||= generate_source_id
|
||||
create_contact_inbox if source_id.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -19,23 +18,37 @@ class ContactInboxBuilder
|
|||
when 'Channel::Whatsapp'
|
||||
wa_source_id
|
||||
when 'Channel::Email'
|
||||
@contact.email
|
||||
email_source_id
|
||||
when 'Channel::Sms'
|
||||
@contact.phone_number
|
||||
when 'Channel::Api'
|
||||
phone_source_id
|
||||
when 'Channel::Api', 'Channel::WebWidget'
|
||||
SecureRandom.uuid
|
||||
else
|
||||
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
|
||||
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
|
||||
return unless @contact.phone_number
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
# whatsapp doesn't want the + in e164 format
|
||||
@contact.phone_number.delete('+').to_s
|
||||
end
|
||||
|
||||
def twilio_source_id
|
||||
return unless @contact.phone_number
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
case @inbox.channel.medium
|
||||
when 'sms'
|
||||
|
@ -45,11 +58,11 @@ class ContactInboxBuilder
|
|||
end
|
||||
end
|
||||
|
||||
def create_contact_inbox(source_id)
|
||||
::ContactInbox.find_or_create_by!(
|
||||
def create_contact_inbox
|
||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
||||
contact_id: @contact.id,
|
||||
inbox_id: @inbox.id,
|
||||
source_id: source_id
|
||||
source_id: @source_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,25 +1,47 @@
|
|||
class ContactBuilder
|
||||
pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified]
|
||||
# This Builder will create a contact and contact inbox with specified attributes.
|
||||
# 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
|
||||
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
|
||||
return contact_inbox if contact_inbox
|
||||
find_or_create_contact_and_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
|
||||
|
||||
private
|
||||
|
||||
def build_contact_with_contact_inbox
|
||||
@contact = find_contact || create_contact
|
||||
@contact_inbox = create_contact_inbox
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= inbox.account
|
||||
end
|
||||
|
||||
def create_contact_inbox(contact)
|
||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id,
|
||||
source_id: source_id
|
||||
)
|
||||
def create_contact_inbox
|
||||
ContactInboxBuilder.new(
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: @source_id,
|
||||
hmac_verified: hmac_verified
|
||||
).perform
|
||||
end
|
||||
|
||||
def update_contact_avatar(contact)
|
||||
|
@ -61,16 +83,4 @@ class ContactBuilder
|
|||
|
||||
account.contacts.find_by(phone_number: phone_number)
|
||||
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
|
|
@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
return if @inbox.channel.reauthorization_required?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
build_contact
|
||||
build_contact_inbox
|
||||
build_message
|
||||
end
|
||||
ensure_contact_avatar
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
rescue StandardError => e
|
||||
|
@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
|
||||
private
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
def build_contact_inbox
|
||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: @sender_id,
|
||||
inbox: @inbox,
|
||||
contact_attributes: contact_params
|
||||
).perform
|
||||
end
|
||||
|
||||
def build_message
|
||||
|
@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
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
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
))
|
||||
|
@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id
|
||||
contact_id: @contact_inbox.contact_id
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
message_type: @message_type,
|
||||
content: response.content,
|
||||
source_id: response.identifier,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
avatar_url: result['profile_pic']
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
return if message_content.blank? && all_unsupported_files?
|
||||
|
||||
@message = conversation.messages.create!(message_params)
|
||||
|
||||
|
@ -117,6 +118,13 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||
cw_message.present?
|
||||
end
|
||||
|
||||
def all_unsupported_files?
|
||||
return if attachments.empty?
|
||||
|
||||
attachments_type = attachments.pluck(:type).uniq.first
|
||||
unsupported_file_type?(attachments_type)
|
||||
end
|
||||
|
||||
### Sample response
|
||||
# {
|
||||
# "object": "instagram",
|
||||
|
|
|
@ -35,7 +35,13 @@ class Messages::MessageBuilder
|
|||
file: uploaded_attachment
|
||||
)
|
||||
|
||||
attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
attachment.file_type = if uploaded_attachment.is_a?(String)
|
||||
file_type_by_signed_id(
|
||||
uploaded_attachment
|
||||
)
|
||||
else
|
||||
file_type(uploaded_attachment&.content_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@ class Messages::Messenger::MessageBuilder
|
|||
include ::FileTypeHelper
|
||||
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
# This check handles very rare case if there are multiple files to attach with only one usupported file
|
||||
return if unsupported_file_type?(attachment['type'])
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
|
@ -80,4 +81,10 @@ class Messages::Messenger::MessageBuilder
|
|||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||
{}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unsupported_file_type?(attachment_type)
|
||||
[:template, :unsupported_type].include? attachment_type.to_sym
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@articles_count = @portal.articles.count
|
||||
@articles = @portal.articles
|
||||
@articles = @articles.search(list_params) if list_params.present?
|
||||
@portal_articles = @portal.articles
|
||||
@all_articles = @portal_articles.search(list_params)
|
||||
@articles_count = @all_articles.count
|
||||
@articles = @all_articles.page(@current_page)
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -37,7 +38,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def portal
|
||||
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
||||
@portal ||= Current.account.portals.find_by!(slug: params[:portal_id])
|
||||
end
|
||||
|
||||
def article_params
|
||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
|||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@current_locale = params[:locale]
|
||||
@categories = @portal.categories.search(params)
|
||||
end
|
||||
|
||||
|
|
|
@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
|
|||
before_action :ensure_inbox, only: [:create]
|
||||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||
@contact_inbox = ContactInboxBuilder.new(
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: params[:source_id]
|
||||
).perform
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
return if params[:inbox_id].blank?
|
||||
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id)
|
||||
ContactInboxBuilder.new(
|
||||
contact: @contact,
|
||||
inbox: inbox,
|
||||
source_id: params[:source_id]
|
||||
).perform
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
|
|
|
@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
include DateRangeHelper
|
||||
|
||||
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
|
||||
result = conversation_finder.perform
|
||||
|
@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
authorize @conversation.inbox, :show?
|
||||
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
|
||||
@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])
|
||||
authorize @contact_inbox.inbox, :show?
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
return if params[:contact_id].blank? || params[:inbox_id].blank?
|
||||
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
authorize inbox, :show?
|
||||
return if @inbox.blank? || @contact.blank?
|
||||
|
||||
ContactInboxBuilder.new(
|
||||
contact_id: params[:contact_id],
|
||||
inbox_id: inbox.id,
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: params[:source_id]
|
||||
).perform
|
||||
end
|
||||
|
|
|
@ -14,6 +14,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
||||
|
||||
@macro.save!
|
||||
process_attachments
|
||||
@macro
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -25,10 +27,21 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||
head :ok
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@macro.update!(macros_with_user)
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
process_attachments
|
||||
@macro.save!
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
|
@ -42,6 +55,17 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||
head :ok
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||
return if actions.blank?
|
||||
|
||||
actions.each do |action|
|
||||
blob_id = action['action_params']
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
@macro.files.attach(blob)
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:name, :account_id, :visibility,
|
||||
|
|
|
@ -14,7 +14,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||
@portal.members << agents
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@all_articles = @portal.articles
|
||||
@articles = @all_articles.search(locale: params[:locale])
|
||||
end
|
||||
|
||||
def create
|
||||
@portal = Current.account.portals.build(portal_params)
|
||||
|
|
|
@ -17,7 +17,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
@contact = ContactIdentifyAction.new(
|
||||
contact: @contact,
|
||||
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
|
||||
retain_original_contact_name: true
|
||||
retain_original_contact_name: true,
|
||||
discard_invalid_attrs: true
|
||||
).perform
|
||||
end
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ module RequestExceptionHandler
|
|||
render_not_found_error('Resource could not be found')
|
||||
rescue Pundit::NotAuthorizedError
|
||||
render_unauthorized('You are not authorized to do this action')
|
||||
rescue ActionController::ParameterMissing => e
|
||||
render_could_not_create_error(e.message)
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
Current.reset
|
||||
|
|
|
@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||
@recoverable = User.find_by(reset_password_token: reset_password_token)
|
||||
if @recoverable && reset_password_and_confirmation(@recoverable)
|
||||
send_auth_headers(@recoverable)
|
||||
render partial: 'devise/auth.json', locals: { resource: @recoverable }
|
||||
render partial: 'devise/auth', formats: [:json], locals: { resource: @recoverable }
|
||||
else
|
||||
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
|
||||
end
|
||||
|
|
|
@ -3,16 +3,12 @@ class Platform::Api::V1::AccountsController < PlatformController
|
|||
@resource = Account.new(account_params)
|
||||
@resource.save!
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
render json: @resource
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @resource
|
||||
end
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.update!(account_params)
|
||||
render json: @resource
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
@ -27,6 +23,14 @@ class Platform::Api::V1::AccountsController < PlatformController
|
|||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:name, :locale)
|
||||
if permitted_params[:enabled_features]
|
||||
return permitted_params.except(:enabled_features).merge(selected_feature_flags: permitted_params[:enabled_features].map(&:to_sym))
|
||||
end
|
||||
|
||||
permitted_params
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :locale, enabled_features: [], limits: {})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ::ContactBuilder.new(
|
||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: source_id,
|
||||
inbox: @inbox_channel.inbox,
|
||||
contact_attributes: permitted_params.except(:identifier, :identifier_hash)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :portal
|
||||
before_action :set_category
|
||||
before_action :set_category, except: [:index]
|
||||
before_action :set_article, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
|
@ -20,7 +20,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
|||
end
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(slug: params[:category_slug])
|
||||
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
|
||||
end
|
||||
|
||||
def portal
|
||||
|
|
|
@ -8,6 +8,12 @@ module FileTypeHelper
|
|||
:file
|
||||
end
|
||||
|
||||
# Used in case of DIRECT_UPLOADS_ENABLED=true
|
||||
def file_type_by_signed_id(signed_id)
|
||||
blob = ActiveStorage::Blob.find_signed(signed_id)
|
||||
file_type(blob&.content_type)
|
||||
end
|
||||
|
||||
def image_file?(content_type)
|
||||
[
|
||||
'image/jpeg',
|
||||
|
|
9
app/javascript/dashboard/api/agentBots.js
Normal file
9
app/javascript/dashboard/api/agentBots.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ApiClient from './ApiClient';
|
||||
|
||||
class AgentBotsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('agent_bots', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new AgentBotsAPI();
|
|
@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI {
|
|||
super('categories', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ portalSlug }) {
|
||||
return axios.get(`${this.url}/${portalSlug}/categories`);
|
||||
get({ portalSlug, locale }) {
|
||||
return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`);
|
||||
}
|
||||
|
||||
create({ portalSlug, categoryObj }) {
|
||||
|
|
|
@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient {
|
|||
super('portals', { accountScoped: true });
|
||||
}
|
||||
|
||||
getPortal({ portalSlug, locale }) {
|
||||
return axios.get(`${this.url}/${portalSlug}?locale=${locale}`);
|
||||
}
|
||||
|
||||
updatePortal({ portalSlug, portalObj }) {
|
||||
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
|
||||
}
|
||||
|
|
16
app/javascript/dashboard/api/macros.js
Normal file
16
app/javascript/dashboard/api/macros.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class MacrosAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('macros', { accountScoped: true });
|
||||
}
|
||||
|
||||
executeMacro({ macroId, conversationIds }) {
|
||||
return axios.post(`${this.url}/${macroId}/execute`, {
|
||||
conversation_ids: conversationIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new MacrosAPI();
|
13
app/javascript/dashboard/api/specs/agentBots.spec.js
Normal file
13
app/javascript/dashboard/api/specs/agentBots.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import AgentBotsAPI from '../agentBots';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#AgentBotsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(AgentBotsAPI).toBeInstanceOf(ApiClient);
|
||||
expect(AgentBotsAPI).toHaveProperty('get');
|
||||
expect(AgentBotsAPI).toHaveProperty('show');
|
||||
expect(AgentBotsAPI).toHaveProperty('create');
|
||||
expect(AgentBotsAPI).toHaveProperty('update');
|
||||
expect(AgentBotsAPI).toHaveProperty('delete');
|
||||
});
|
||||
});
|
14
app/javascript/dashboard/api/specs/macros.spec.js
Normal file
14
app/javascript/dashboard/api/specs/macros.spec.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import macros from '../macros';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#macrosAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(macros).toBeInstanceOf(ApiClient);
|
||||
expect(macros).toHaveProperty('get');
|
||||
expect(macros).toHaveProperty('create');
|
||||
expect(macros).toHaveProperty('update');
|
||||
expect(macros).toHaveProperty('delete');
|
||||
expect(macros).toHaveProperty('show');
|
||||
expect(macros.url).toBe('/api/v1/macros');
|
||||
});
|
||||
});
|
|
@ -113,9 +113,22 @@ $default-button-height: 4.0rem;
|
|||
}
|
||||
|
||||
&.clear {
|
||||
color: var(--w-700);
|
||||
|
||||
&.secondary {
|
||||
color: var(--s-700)
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--g-700)
|
||||
}
|
||||
|
||||
&.alert {
|
||||
color: var(--r-700)
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--y-600);
|
||||
color: var(--y-700)
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -146,6 +159,8 @@ $default-button-height: 4.0rem;
|
|||
|
||||
&.small {
|
||||
height: var(--space-large);
|
||||
padding-bottom: var(--space-smaller);
|
||||
padding-top: var(--space-smaller);
|
||||
}
|
||||
|
||||
&.large {
|
||||
|
|
|
@ -14,15 +14,9 @@
|
|||
}
|
||||
|
||||
.modal--close {
|
||||
border-radius: 50%;
|
||||
color: $color-heading;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-big;
|
||||
line-height: $space-normal;
|
||||
padding: $space-normal;
|
||||
position: absolute;
|
||||
right: $space-micro;
|
||||
top: $space-micro;
|
||||
right: $space-small;
|
||||
top: $space-small;
|
||||
|
||||
&:hover {
|
||||
background: $color-background;
|
||||
|
|
|
@ -7,9 +7,13 @@
|
|||
@click="onBackDropClick"
|
||||
>
|
||||
<div :class="modalContainerClassName" @click.stop>
|
||||
<button class="modal--close" @click="close">
|
||||
<fluent-icon icon="dismiss" />
|
||||
</button>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
class="modal--close"
|
||||
@click="close"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,9 +5,12 @@ import Button from './ui/WootButton';
|
|||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||
import ContextMenu from './ui/ContextMenu.vue';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import FeatureToggle from './widgets/FeatureToggle';
|
||||
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
import Label from './ui/Label';
|
||||
|
@ -21,8 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton';
|
|||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||
import ContextMenu from './ui/ContextMenu.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
AvatarUploader,
|
||||
|
@ -31,9 +32,12 @@ const WootUIKit = {
|
|||
Code,
|
||||
ColorPicker,
|
||||
ConfirmDeleteModal,
|
||||
ConfirmModal,
|
||||
ContextMenu,
|
||||
DeleteModal,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
FeatureToggle,
|
||||
HorizontalBar,
|
||||
Input,
|
||||
Label,
|
||||
|
@ -47,8 +51,6 @@ const WootUIKit = {
|
|||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
ConfirmModal,
|
||||
ContextMenu,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
|
|
@ -73,14 +73,14 @@ export default {
|
|||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentRole: 'getCurrentRole',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
labels: 'labels/getLabelsOnSidebar',
|
||||
teams: 'teams/getMyTeams',
|
||||
}),
|
||||
|
|
|
@ -39,7 +39,7 @@ const primaryMenuItems = accountId => [
|
|||
label: 'HELP_CENTER.TITLE',
|
||||
featureFlag: 'help_center',
|
||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||
toStateName: 'list_all_portals',
|
||||
toStateName: 'default_portal_articles',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,45 +1,58 @@
|
|||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const settings = accountId => ({
|
||||
parentNav: 'settings',
|
||||
routes: [
|
||||
'agent_bots',
|
||||
'agent_list',
|
||||
'canned_list',
|
||||
'labels_list',
|
||||
'settings_inbox',
|
||||
'attributes_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_show',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inbox_finish',
|
||||
'settings_integrations',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'settings_applications',
|
||||
'settings_integrations_dashboard_apps',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications_integration',
|
||||
'general_settings',
|
||||
'automation_list',
|
||||
'billing_settings_index',
|
||||
'canned_list',
|
||||
'general_settings_index',
|
||||
'general_settings',
|
||||
'labels_list',
|
||||
'macros_edit',
|
||||
'macros_new',
|
||||
'macros_wrapper',
|
||||
'settings_applications_integration',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications',
|
||||
'settings_inbox_finish',
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_show',
|
||||
'settings_inbox',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_integrations_dashboard_apps',
|
||||
'settings_integrations_integration',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations',
|
||||
'settings_teams_add_agents',
|
||||
'settings_teams_edit_finish',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit',
|
||||
'settings_teams_finish',
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'settings_teams_add_agents',
|
||||
'settings_teams_finish',
|
||||
'settings_teams_edit',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit_finish',
|
||||
'billing_settings_index',
|
||||
'automation_list',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
icon: 'briefcase',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
{
|
||||
icon: 'people',
|
||||
label: 'AGENTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||
toStateName: 'agent_list',
|
||||
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
icon: 'people-team',
|
||||
|
@ -47,6 +60,7 @@ const settings = accountId => ({
|
|||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||
toStateName: 'settings_teams_list',
|
||||
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
icon: 'mail-inbox-all',
|
||||
|
@ -54,6 +68,7 @@ const settings = accountId => ({
|
|||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
icon: 'tag',
|
||||
|
@ -61,6 +76,7 @@ const settings = accountId => ({
|
|||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||
toStateName: 'labels_list',
|
||||
featureFlag: FEATURE_FLAGS.LABELS,
|
||||
},
|
||||
{
|
||||
icon: 'code',
|
||||
|
@ -70,13 +86,34 @@ const settings = accountId => ({
|
|||
`accounts/${accountId}/settings/custom-attributes/list`
|
||||
),
|
||||
toStateName: 'attributes_list',
|
||||
featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES,
|
||||
},
|
||||
{
|
||||
icon: 'automation',
|
||||
label: 'AUTOMATION',
|
||||
beta: true,
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||
toStateName: 'automation_list',
|
||||
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'bot',
|
||||
label: 'AGENT_BOTS',
|
||||
beta: true,
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||
toStateName: 'agent_bots',
|
||||
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
||||
},
|
||||
{
|
||||
icon: 'flash-settings',
|
||||
label: 'MACROS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/macros`),
|
||||
toStateName: 'macros_wrapper',
|
||||
beta: true,
|
||||
featureFlag: FEATURE_FLAGS.MACROS,
|
||||
},
|
||||
{
|
||||
icon: 'chat-multiple',
|
||||
|
@ -86,6 +123,7 @@ const settings = accountId => ({
|
|||
`accounts/${accountId}/settings/canned-response/list`
|
||||
),
|
||||
toStateName: 'canned_list',
|
||||
featureFlag: FEATURE_FLAGS.CANNED_RESPONSES,
|
||||
},
|
||||
{
|
||||
icon: 'flash-on',
|
||||
|
@ -93,6 +131,7 @@ const settings = accountId => ({
|
|||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'star-emphasis',
|
||||
|
@ -100,6 +139,7 @@ const settings = accountId => ({
|
|||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'credit-card-person',
|
||||
|
@ -109,13 +149,6 @@ const settings = accountId => ({
|
|||
toStateName: 'billing_settings_index',
|
||||
showOnlyOnCloud: true,
|
||||
},
|
||||
{
|
||||
icon: 'settings',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||
import AccountContext from './AccountContext.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -61,6 +63,9 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
hasSecondaryMenu() {
|
||||
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
|
||||
},
|
||||
|
@ -89,7 +94,7 @@ export default {
|
|||
icon: 'folder',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: true,
|
||||
newLink: true,
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.INBOX_MANAGEMENT),
|
||||
newLinkTag: 'NEW_INBOX',
|
||||
key: 'inbox',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
||||
|
@ -117,7 +122,7 @@ export default {
|
|||
icon: 'number-symbol',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: true,
|
||||
newLink: true,
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||
newLinkTag: 'NEW_LABEL',
|
||||
key: 'label',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||
|
@ -141,7 +146,7 @@ export default {
|
|||
label: 'TAGGED_WITH',
|
||||
hasSubMenu: true,
|
||||
key: 'label',
|
||||
newLink: true,
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||
newLinkTag: 'NEW_LABEL',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||
toStateName: 'labels_list',
|
||||
|
@ -163,7 +168,7 @@ export default {
|
|||
icon: 'people-team',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: true,
|
||||
newLink: true,
|
||||
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||
newLinkTag: 'NEW_TEAM',
|
||||
key: 'team',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`),
|
||||
|
@ -238,6 +243,9 @@ export default {
|
|||
toggleAccountModal() {
|
||||
this.$emit('toggle-accounts');
|
||||
},
|
||||
showNewLink(featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, featureFlag);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
:class="{ 'text-truncate': shouldTruncate }"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="isHelpCenterSidebar && childItemCount" class="count-view">
|
||||
<span v-if="showChildCount" class="count-view">
|
||||
{{ childItemCount }}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -76,7 +76,7 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isHelpCenterSidebar: {
|
||||
showChildCount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -127,11 +127,16 @@ $label-badge-size: var(--space-slab);
|
|||
color: var(--w-500);
|
||||
border-color: var(--w-25);
|
||||
}
|
||||
&.is-active .count-view {
|
||||
background: var(--w-75);
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex-grow: 1;
|
||||
line-height: var(--space-two);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inbox-icon {
|
||||
|
@ -175,10 +180,6 @@ $label-badge-size: var(--space-slab);
|
|||
font-weight: var(--font-weight-bold);
|
||||
margin-left: var(--space-smaller);
|
||||
padding: var(--space-zero) var(--space-smaller);
|
||||
|
||||
&.is-active {
|
||||
background: var(--w-50);
|
||||
color: var(--w-500);
|
||||
}
|
||||
line-height: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
<template>
|
||||
<li class="sidebar-item">
|
||||
<li v-show="isMenuItemVisible" class="sidebar-item">
|
||||
<div v-if="hasSubMenu" class="secondary-menu--wrap">
|
||||
<span class="secondary-menu--header fs-small">
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
</span>
|
||||
<div v-if="isHelpCenterSidebar" class="submenu-icons">
|
||||
<div v-if="menuItem.showNewButton" class="submenu-icons">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
class="submenu-icon"
|
||||
@click="onClickOpen"
|
||||
>
|
||||
<fluent-icon icon="add" size="16" />
|
||||
</woot-button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
|
@ -28,15 +27,11 @@
|
|||
size="14"
|
||||
/>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
<span
|
||||
v-if="isHelpCenterSidebar"
|
||||
class="count-view"
|
||||
:class="computedClass"
|
||||
>
|
||||
<span v-if="showChildCount(menuItem.count)" class="count-view">
|
||||
{{ `${menuItem.count}` }}
|
||||
</span>
|
||||
<span
|
||||
v-if="menuItem.label === 'AUTOMATION'"
|
||||
v-if="menuItem.beta"
|
||||
data-view-component="true"
|
||||
label="Beta"
|
||||
class="beta"
|
||||
|
@ -55,7 +50,7 @@
|
|||
:should-truncate="child.truncateLabel"
|
||||
:icon="computedInboxClass(child)"
|
||||
:warning-icon="computedInboxErrorClass(child)"
|
||||
:is-help-center-sidebar="isHelpCenterSidebar"
|
||||
:show-child-count="showChildCount(child.count)"
|
||||
:child-item-count="child.count"
|
||||
/>
|
||||
<router-link
|
||||
|
@ -64,10 +59,10 @@
|
|||
:to="menuItem.toState"
|
||||
custom
|
||||
>
|
||||
<li>
|
||||
<li class="menu-item--new">
|
||||
<a
|
||||
:href="href"
|
||||
class="button small clear menu-item--new secondary"
|
||||
class="button small link clear secondary"
|
||||
:class="{ 'is-active': isActive }"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
|
@ -78,9 +73,6 @@
|
|||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
<p v-if="isHelpCenterSidebar && isCategoryEmpty" class="empty-text">
|
||||
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
||||
</p>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -104,20 +96,25 @@ export default {
|
|||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isHelpCenterSidebar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCategoryEmpty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ activeInbox: 'getSelectedInbox' }),
|
||||
...mapGetters({
|
||||
activeInbox: 'getSelectedInbox',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
hasSubMenu() {
|
||||
return !!this.menuItem.children;
|
||||
},
|
||||
isMenuItemVisible() {
|
||||
if (!this.menuItem.featureFlag) {
|
||||
return true;
|
||||
}
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
this.menuItem.featureFlag
|
||||
);
|
||||
},
|
||||
isInboxConversation() {
|
||||
return (
|
||||
this.$store.state.route.name === 'inbox_conversation' &&
|
||||
|
@ -148,8 +145,8 @@ export default {
|
|||
this.menuItem.toStateName === 'settings_applications'
|
||||
);
|
||||
},
|
||||
isArticlesView() {
|
||||
return this.$store.state.route.name === this.menuItem.toStateName;
|
||||
isCurrentRoute() {
|
||||
return this.$store.state.route.name.includes(this.menuItem.toStateName);
|
||||
},
|
||||
|
||||
computedClass() {
|
||||
|
@ -168,12 +165,11 @@ export default {
|
|||
}
|
||||
return ' ';
|
||||
}
|
||||
if (this.isHelpCenterSidebar) {
|
||||
if (this.isArticlesView) {
|
||||
return 'is-active';
|
||||
}
|
||||
return ' ';
|
||||
|
||||
if (this.isCurrentRoute) {
|
||||
return 'is-active';
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
@ -204,11 +200,14 @@ export default {
|
|||
}
|
||||
},
|
||||
showItem(item) {
|
||||
return this.isAdmin && item.newLink !== undefined;
|
||||
return this.isAdmin && !!item.newLink;
|
||||
},
|
||||
onClickOpen() {
|
||||
this.$emit('open');
|
||||
},
|
||||
showChildCount(count) {
|
||||
return Number.isInteger(count);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -264,6 +263,11 @@ export default {
|
|||
color: var(--w-500);
|
||||
border-color: var(--w-25);
|
||||
}
|
||||
|
||||
&.is-active .count-view {
|
||||
background: var(--w-75);
|
||||
color: var(--w-600);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-menu--icon {
|
||||
|
@ -293,22 +297,19 @@ export default {
|
|||
top: -1px;
|
||||
}
|
||||
|
||||
.sidebar-item .button.menu-item--new {
|
||||
display: inline-flex;
|
||||
height: var(--space-medium);
|
||||
margin: var(--space-smaller) 0;
|
||||
padding: var(--space-smaller);
|
||||
color: var(--s-500);
|
||||
.sidebar-item .menu-item--new {
|
||||
padding: var(--space-small) 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--w-500);
|
||||
.button {
|
||||
display: inline-flex;
|
||||
color: var(--s-500);
|
||||
}
|
||||
}
|
||||
|
||||
.beta {
|
||||
padding-right: var(--space-smaller) !important;
|
||||
padding-left: var(--space-smaller) !important;
|
||||
margin-left: var(--space-half) !important;
|
||||
margin-left: var(--space-smaller) !important;
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-medium);
|
||||
|
@ -327,11 +328,6 @@ export default {
|
|||
font-weight: var(--font-weight-bold);
|
||||
margin-left: var(--space-smaller);
|
||||
padding: var(--space-zero) var(--space-smaller);
|
||||
|
||||
&.is-active {
|
||||
background: var(--w-50);
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-icons {
|
||||
|
@ -343,10 +339,4 @@ export default {
|
|||
margin-left: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-small);
|
||||
margin: var(--space-smaller);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
<template>
|
||||
<span class="time-ago">
|
||||
<span> {{ timeAgo }}</span>
|
||||
<span>{{ timeAgo }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ZERO = 0;
|
||||
const MINUTE_IN_MILLI_SECONDS = 60000;
|
||||
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
||||
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
||||
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { differenceInMilliseconds } from 'date-fns';
|
||||
|
||||
export default {
|
||||
name: 'TimeAgo',
|
||||
|
@ -28,51 +26,40 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
timeAgo: '',
|
||||
timeAgo: this.dynamicTime(this.timestamp),
|
||||
timer: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
watch: {
|
||||
timestamp() {
|
||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.isAutoRefreshEnabled) {
|
||||
this.createTimer();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timer);
|
||||
},
|
||||
methods: {
|
||||
createTimer() {
|
||||
this.timer = setTimeout(() => {
|
||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||
this.createTimer();
|
||||
}, this.refreshTime());
|
||||
},
|
||||
refreshTime() {
|
||||
const timeDiff = differenceInMilliseconds(
|
||||
new Date(),
|
||||
new Date(this.timestamp * 1000)
|
||||
);
|
||||
const timeDiff = Date.now() - this.timestamp * 1000;
|
||||
if (timeDiff > DAY_IN_MILLI_SECONDS) {
|
||||
return DAY_IN_MILLI_SECONDS;
|
||||
}
|
||||
if (timeDiff > HOUR_IN_MILLI_SECONDS) {
|
||||
return HOUR_IN_MILLI_SECONDS;
|
||||
}
|
||||
if (timeDiff > MINUTE_IN_MILLI_SECONDS) {
|
||||
return MINUTE_IN_MILLI_SECONDS;
|
||||
}
|
||||
return ZERO;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||
if (this.isAutoRefreshEnabled) {
|
||||
this.createTimer();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearTimer();
|
||||
},
|
||||
methods: {
|
||||
createTimer() {
|
||||
const refreshTime = this.refreshTime;
|
||||
if (refreshTime > ZERO) {
|
||||
this.timer = setTimeout(() => {
|
||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||
this.createTimer();
|
||||
}, refreshTime);
|
||||
}
|
||||
},
|
||||
clearTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
return MINUTE_IN_MILLI_SECONDS;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
class="filter"
|
||||
:class="{ error: v.action_params.$dirty && v.action_params.$error }"
|
||||
>
|
||||
<div class="filter" :class="actionInputStyles">
|
||||
<div class="filter-inputs">
|
||||
<select
|
||||
v-model="action_name"
|
||||
|
@ -60,6 +57,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="!isMacro"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
|
@ -120,6 +118,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isMacro: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
action_name: {
|
||||
|
@ -146,6 +148,12 @@ export default {
|
|||
return this.actionTypes.find(action => action.key === this.action_name)
|
||||
.inputType;
|
||||
},
|
||||
actionInputStyles() {
|
||||
return {
|
||||
'has-error': this.v.action_params.$dirty && this.v.action_params.$error,
|
||||
'is-a-macro': this.isMacro,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeAction() {
|
||||
|
@ -165,9 +173,21 @@ export default {
|
|||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
margin-bottom: var(--space-small);
|
||||
|
||||
&.is-a-macro {
|
||||
margin-bottom: 0;
|
||||
background: var(--white);
|
||||
padding: var(--space-zero);
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.filter.error {
|
||||
.no-margin-bottom {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter.has-error {
|
||||
background: var(--r-50);
|
||||
}
|
||||
|
||||
|
|
|
@ -113,5 +113,6 @@ input[type='file'] {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="avatar-container"
|
||||
:style="[style, customStyle]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span>{{ userInitial }}</span>
|
||||
<div class="avatar-container" :style="style" aria-hidden="true">
|
||||
{{ userInitial }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -16,69 +12,26 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#c2e1ff',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#1976cc',
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let style = {
|
||||
width: `${this.size}px`,
|
||||
height: `${this.size}px`,
|
||||
borderRadius:
|
||||
this.variant === 'square' ? 'var(--border-radius-large)' : '50%',
|
||||
lineHeight: `${this.size + Math.floor(this.size / 20)}px`,
|
||||
return {
|
||||
fontSize: `${Math.floor(this.size / 2.5)}px`,
|
||||
};
|
||||
|
||||
if (this.backgroundColor) {
|
||||
style = { ...style, backgroundColor: this.backgroundColor };
|
||||
}
|
||||
if (this.color) {
|
||||
style = { ...style, color: this.color };
|
||||
}
|
||||
return style;
|
||||
},
|
||||
userInitial() {
|
||||
return this.initials || this.initial(this.username);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initial(username) {
|
||||
const parts = username ? username.split(/[ -]/) : [];
|
||||
let initials = '';
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
initials += parts[i].charAt(0);
|
||||
}
|
||||
const parts = this.username.split(/[ -]/);
|
||||
let initials = parts.reduce((acc, curr) => acc + curr.charAt(0), '');
|
||||
|
||||
if (initials.length > 2 && initials.search(/[A-Z]/) !== -1) {
|
||||
initials = initials.replace(/[a-z]+/g, '');
|
||||
}
|
||||
initials = initials.substr(0, 2).toUpperCase();
|
||||
initials = initials.substring(0, 2).toUpperCase();
|
||||
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
|
@ -88,6 +41,7 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
line-height: 100%;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div v-if="isFeatureEnabled">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
props: {
|
||||
featureKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
isFeatureEnabled() {
|
||||
return this.isFeatureEnabledonAccount(this.accountId, this.featureKey);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -83,75 +83,71 @@ export default {
|
|||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 15,
|
||||
default: 25,
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
onPageChange: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isFooterVisible() {
|
||||
return this.totalCount && !(this.firstIndex > this.totalCount);
|
||||
},
|
||||
firstIndex() {
|
||||
const firstIndex = this.pageSize * (this.currentPage - 1) + 1;
|
||||
return firstIndex;
|
||||
return this.pageSize * (this.currentPage - 1) + 1;
|
||||
},
|
||||
lastIndex() {
|
||||
const index = Math.min(this.totalCount, this.pageSize * this.currentPage);
|
||||
return index;
|
||||
return Math.min(this.totalCount, this.pageSize * this.currentPage);
|
||||
},
|
||||
searchButtonClass() {
|
||||
return this.searchQuery !== '' ? 'show' : '';
|
||||
},
|
||||
hasLastPage() {
|
||||
const isDisabled =
|
||||
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||
return isDisabled;
|
||||
return !!Math.ceil(this.totalCount / this.pageSize);
|
||||
},
|
||||
hasFirstPage() {
|
||||
const isDisabled = this.currentPage === 1;
|
||||
return isDisabled;
|
||||
return this.currentPage === 1;
|
||||
},
|
||||
hasNextPage() {
|
||||
const isDisabled =
|
||||
this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||
return isDisabled;
|
||||
return this.currentPage === Math.ceil(this.totalCount / this.pageSize);
|
||||
},
|
||||
hasPrevPage() {
|
||||
const isDisabled = this.currentPage === 1;
|
||||
return isDisabled;
|
||||
return this.currentPage === 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNextPage() {
|
||||
if (this.hasNextPage) return;
|
||||
if (this.hasNextPage) {
|
||||
return;
|
||||
}
|
||||
const newPage = this.currentPage + 1;
|
||||
this.onPageChange(newPage);
|
||||
},
|
||||
onPrevPage() {
|
||||
if (this.hasPrevPage) return;
|
||||
|
||||
if (this.hasPrevPage) {
|
||||
return;
|
||||
}
|
||||
const newPage = this.currentPage - 1;
|
||||
this.onPageChange(newPage);
|
||||
},
|
||||
onFirstPage() {
|
||||
if (this.hasFirstPage) return;
|
||||
|
||||
if (this.hasFirstPage) {
|
||||
return;
|
||||
}
|
||||
const newPage = 1;
|
||||
this.onPageChange(newPage);
|
||||
},
|
||||
onLastPage() {
|
||||
if (this.hasLastPage) return;
|
||||
|
||||
if (this.hasLastPage) {
|
||||
return;
|
||||
}
|
||||
const newPage = Math.ceil(this.totalCount / this.pageSize);
|
||||
this.onPageChange(newPage);
|
||||
},
|
||||
onPageChange(page) {
|
||||
this.$emit('page-change', page);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -2,8 +2,8 @@ import { mount } from '@vue/test-utils';
|
|||
import Avatar from './Avatar.vue';
|
||||
import Thumbnail from './Thumbnail.vue';
|
||||
|
||||
describe(`when there are NO errors loading the thumbnail`, () => {
|
||||
it(`should render the agent thumbnail`, () => {
|
||||
describe('Thumbnail.vue', () => {
|
||||
it('should render the agent thumbnail if valid image is passed', () => {
|
||||
const wrapper = mount(Thumbnail, {
|
||||
propsData: {
|
||||
src: 'https://some_valid_url.com',
|
||||
|
@ -14,14 +14,12 @@ describe(`when there are NO errors loading the thumbnail`, () => {
|
|||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('#image').exists()).toBe(true);
|
||||
expect(wrapper.find('.user-thumbnail').exists()).toBe(true);
|
||||
const avatarComponent = wrapper.findComponent(Avatar);
|
||||
expect(avatarComponent.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there ARE errors loading the thumbnail`, () => {
|
||||
it(`should render the agent avatar`, () => {
|
||||
it('should render the avatar component if invalid image is passed', () => {
|
||||
const wrapper = mount(Thumbnail, {
|
||||
propsData: {
|
||||
src: 'https://some_invalid_url.com',
|
||||
|
@ -32,19 +30,17 @@ describe(`when there ARE errors loading the thumbnail`, () => {
|
|||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('#image').exists()).toBe(false);
|
||||
expect(wrapper.find('.avatar-container').exists()).toBe(true);
|
||||
const avatarComponent = wrapper.findComponent(Avatar);
|
||||
expect(avatarComponent.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when Avatar shows`, () => {
|
||||
it(`initials shold correspond to username`, () => {
|
||||
it('should the initial of the name if no image is passed', () => {
|
||||
const wrapper = mount(Avatar, {
|
||||
propsData: {
|
||||
username: 'Angie Rojas',
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('span').text()).toBe('AR');
|
||||
expect(wrapper.find('div').text()).toBe('AR');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,74 +1,23 @@
|
|||
<template>
|
||||
<div class="user-thumbnail-box" :style="{ height: size, width: size }">
|
||||
<img
|
||||
v-if="!imgError && Boolean(src)"
|
||||
id="image"
|
||||
v-if="!imgError && src"
|
||||
:src="src"
|
||||
:class="thumbnailClass"
|
||||
@error="onImgError()"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<Avatar
|
||||
v-else
|
||||
:username="userNameWithoutEmoji"
|
||||
:class="thumbnailClass"
|
||||
:size="avatarSize"
|
||||
:variant="variant"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'instagram_direct_message'"
|
||||
id="badge"
|
||||
v-if="badgeSrc"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/instagram-dm.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'facebook'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/messenger.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'twitter-tweet'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/twitter-tweet.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'twitter-dm'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/twitter-dm.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'whatsapp'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/whatsapp.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'sms'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/sms.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'Channel::Line'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/line.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'Channel::Telegram'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/telegram.png"
|
||||
:src="`/integrations/channels/badges/${badgeSrc}.png`"
|
||||
alt="Badge"
|
||||
/>
|
||||
<div
|
||||
v-if="showStatusIndicator"
|
||||
|
@ -83,7 +32,7 @@
|
|||
* Src - source for round image
|
||||
* Size - Size of the thumbnail
|
||||
* Badge - Chat source indication { fb / telegram }
|
||||
* Username - User name for avatar
|
||||
* Username - Username for avatar
|
||||
*/
|
||||
import Avatar from './Avatar';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
|
@ -103,7 +52,7 @@ export default {
|
|||
},
|
||||
badge: {
|
||||
type: String,
|
||||
default: 'fb',
|
||||
default: '',
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
|
@ -142,6 +91,19 @@ export default {
|
|||
avatarSize() {
|
||||
return Number(this.size.replace(/\D+/g, ''));
|
||||
},
|
||||
badgeSrc() {
|
||||
return {
|
||||
instagram_direct_message: 'instagram-dm',
|
||||
facebook: 'messenger',
|
||||
'twitter-tweet': 'twitter-tweet',
|
||||
'twitter-dm': 'twitter-dm',
|
||||
whatsapp: 'whatsapp',
|
||||
sms: 'sms',
|
||||
'Channel::Line': 'line',
|
||||
'Channel::Telegram': 'telegram',
|
||||
'Channel::WebWidget': '',
|
||||
}[this.badge];
|
||||
},
|
||||
badgeStyle() {
|
||||
const size = Math.floor(this.avatarSize / 3);
|
||||
const badgeSize = `${size + 2}px`;
|
||||
|
@ -160,12 +122,10 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
src: {
|
||||
handler(value, oldValue) {
|
||||
if (value !== oldValue && this.imgError) {
|
||||
this.imgError = false;
|
||||
}
|
||||
},
|
||||
src(value, oldValue) {
|
||||
if (value !== oldValue && this.imgError) {
|
||||
this.imgError = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -229,9 +189,5 @@ export default {
|
|||
.user-online-status--offline {
|
||||
background: var(--s-500);
|
||||
}
|
||||
|
||||
.user-online-status--offline {
|
||||
background: var(--s-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
size="small"
|
||||
@click="toggleEmojiPicker"
|
||||
/>
|
||||
<!-- ensure the same validations for attachment types are implemented in backend models as well -->
|
||||
<file-upload
|
||||
ref="upload"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
|
@ -47,6 +46,16 @@
|
|||
:title="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
|
||||
@click="toggleAudioRecorder"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showEditorToggle"
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
icon="quote"
|
||||
emoji="🖊️"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="$emit('toggle-editor')"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showAudioPlayStopButton"
|
||||
:icon="audioRecorderPlayStopIcon"
|
||||
|
@ -110,13 +119,15 @@ import { hasPressedAltAndAKey } from 'shared/helpers/KeyboardHelpers';
|
|||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import {
|
||||
ALLOWED_FILE_TYPES,
|
||||
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
|
||||
} from 'shared/constants/messages';
|
||||
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload },
|
||||
|
@ -182,7 +193,7 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isFormatMode: {
|
||||
showEditorToggle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -200,6 +211,10 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
isNote() {
|
||||
return this.mode === REPLY_EDITOR_MODES.NOTE;
|
||||
},
|
||||
|
@ -217,7 +232,12 @@ export default {
|
|||
return this.showFileUpload || this.isNote;
|
||||
},
|
||||
showAudioRecorderButton() {
|
||||
return this.showAudioRecorder;
|
||||
return (
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.VOICE_RECORDER
|
||||
) && this.showAudioRecorder
|
||||
);
|
||||
},
|
||||
showAudioPlayStopButton() {
|
||||
return this.showAudioRecorder && this.isRecordingAudio;
|
||||
|
|
|
@ -420,6 +420,8 @@ export default {
|
|||
<style lang="scss">
|
||||
.wrap {
|
||||
> .bubble {
|
||||
min-width: 128px;
|
||||
|
||||
&.is-image,
|
||||
&.is-video {
|
||||
padding: 0;
|
||||
|
|
|
@ -139,7 +139,6 @@ export default {
|
|||
listLoadingStatus: 'getAllMessagesLoaded',
|
||||
getUnreadCount: 'getUnreadCount',
|
||||
loadingChatList: 'getChatListLoadingStatus',
|
||||
conversationLastSeen: 'getConversationLastSeen',
|
||||
}),
|
||||
inboxId() {
|
||||
return this.currentChat.inbox_id;
|
||||
|
@ -234,7 +233,6 @@ export default {
|
|||
return 'arrow-chevron-left';
|
||||
},
|
||||
getLastSeenAt() {
|
||||
if (this.conversationLastSeen) return this.conversationLastSeen;
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt;
|
||||
},
|
||||
|
@ -434,7 +432,7 @@ export default {
|
|||
|
||||
&::before {
|
||||
transform: rotate(0deg);
|
||||
left: var(--space-half);
|
||||
left: var(--space-smaller);
|
||||
bottom: var(--space-minus-slab);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,10 +109,11 @@
|
|||
:recording-audio-state="recordingAudioState"
|
||||
:is-recording-audio="isRecordingAudio"
|
||||
:is-on-private-note="isOnPrivateNote"
|
||||
:is-format-mode="showRichContentEditor"
|
||||
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
|
||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||
:has-whatsapp-templates="hasWhatsappTemplates"
|
||||
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
||||
@toggle-editor="toggleRichContentEditor"
|
||||
/>
|
||||
<whatsapp-templates
|
||||
:inbox-id="inbox.id"
|
||||
|
@ -230,6 +231,13 @@ export default {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (this.isAPIInbox) {
|
||||
const {
|
||||
display_rich_content_editor: displayRichContentEditor = false,
|
||||
} = this.uiSettings;
|
||||
return displayRichContentEditor;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
assignedAgent: {
|
||||
|
@ -365,7 +373,7 @@ export default {
|
|||
);
|
||||
},
|
||||
isRichEditorEnabled() {
|
||||
return this.isAWebWidgetInbox || this.isAnEmailChannel || this.isAPIInbox;
|
||||
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
||||
},
|
||||
showAudioRecorder() {
|
||||
return !this.isOnPrivateNote && this.showFileUpload;
|
||||
|
@ -477,7 +485,7 @@ export default {
|
|||
const hasNextWord = updatedMessage.includes(' ');
|
||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length);
|
||||
this.mentionSearchKey = updatedMessage.substring(1);
|
||||
this.showMentions = true;
|
||||
} else {
|
||||
this.mentionSearchKey = '';
|
||||
|
@ -511,6 +519,11 @@ export default {
|
|||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
methods: {
|
||||
toggleRichContentEditor() {
|
||||
this.updateUISettings({
|
||||
display_rich_content_editor: !this.showRichContentEditor,
|
||||
});
|
||||
},
|
||||
getSavedDraftMessages() {
|
||||
return LocalStorage.get(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES) || {};
|
||||
},
|
||||
|
@ -970,7 +983,7 @@ export default {
|
|||
|
||||
&::before {
|
||||
transform: rotate(0deg);
|
||||
left: var(--space-half);
|
||||
left: var(--space-smaller);
|
||||
bottom: var(--space-minus-slab);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,10 @@
|
|||
@click="snoozeConversation(option.snoozedUntil)"
|
||||
/>
|
||||
</menu-item-with-submenu>
|
||||
<menu-item-with-submenu :option="labelMenuConfig">
|
||||
<menu-item-with-submenu
|
||||
:option="labelMenuConfig"
|
||||
:sub-menu-available="!!labels.length"
|
||||
>
|
||||
<template>
|
||||
<menu-item
|
||||
v-for="label in labels"
|
||||
|
@ -28,7 +31,10 @@
|
|||
/>
|
||||
</template>
|
||||
</menu-item-with-submenu>
|
||||
<menu-item-with-submenu :option="agentMenuConfig">
|
||||
<menu-item-with-submenu
|
||||
:option="agentMenuConfig"
|
||||
:sub-menu-available="!!assignableAgents.length"
|
||||
>
|
||||
<agent-loading-placeholder v-if="assignableAgentsUiFlags.isFetching" />
|
||||
<template v-else>
|
||||
<menu-item
|
||||
|
@ -40,7 +46,10 @@
|
|||
/>
|
||||
</template>
|
||||
</menu-item-with-submenu>
|
||||
<menu-item-with-submenu :option="teamMenuConfig">
|
||||
<menu-item-with-submenu
|
||||
:option="teamMenuConfig"
|
||||
:sub-menu-available="!!teams.length"
|
||||
>
|
||||
<menu-item
|
||||
v-for="team in teams"
|
||||
:key="team.id"
|
||||
|
@ -141,9 +150,19 @@ export default {
|
|||
assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
}),
|
||||
assignableAgents() {
|
||||
return this.$store.getters['inboxAssignableAgents/getAssignableAgents'](
|
||||
this.inboxId
|
||||
);
|
||||
return [
|
||||
{
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: null,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
},
|
||||
...this.$store.getters['inboxAssignableAgents/getAssignableAgents'](
|
||||
this.inboxId
|
||||
),
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
size="20px"
|
||||
class="agent-thumbnail"
|
||||
/>
|
||||
<p class="menu-label truncate-text">{{ option.label }}</p>
|
||||
<p class="menu-label text-truncate">{{ option.label }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -50,7 +50,6 @@ export default {
|
|||
padding: var(--space-smaller);
|
||||
border-radius: var(--border-radius-small);
|
||||
overflow: hidden;
|
||||
|
||||
.menu-label {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-mini);
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<div class="menu-with-submenu flex-between">
|
||||
<div
|
||||
class="menu-with-submenu flex-between"
|
||||
:class="{ disabled: !subMenuAvailable }"
|
||||
>
|
||||
<div class="menu-left">
|
||||
<fluent-icon :icon="option.icon" size="14" class="menu-icon" />
|
||||
<p class="menu-label">{{ option.label }}</p>
|
||||
</div>
|
||||
<fluent-icon icon="chevron-right" size="12" />
|
||||
<div class="submenu">
|
||||
<div v-if="subMenuAvailable" class="submenu">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,6 +21,10 @@ export default {
|
|||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
subMenuAvailable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -55,6 +62,11 @@ export default {
|
|||
left: 100%;
|
||||
top: 0;
|
||||
display: none;
|
||||
min-height: min-content;
|
||||
max-height: var(--space-giga);
|
||||
overflow-y: auto;
|
||||
// Need this because Firefox adds a horizontal scrollbar, if a text is truncated inside.
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -73,5 +85,10 @@ export default {
|
|||
clip-path: polygon(100% 0, 0% 0%, 100% 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 50%;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
<div v-else class="agent-confirmation-container">
|
||||
<p>
|
||||
<p v-if="selectedAgent.id">
|
||||
{{
|
||||
$t('BULK_ACTION.ASSIGN_CONFIRMATION_LABEL', {
|
||||
conversationCount,
|
||||
|
@ -67,6 +67,15 @@
|
|||
<strong>
|
||||
{{ selectedAgent.name }}
|
||||
</strong>
|
||||
<span>?</span>
|
||||
</p>
|
||||
<p v-else>
|
||||
{{
|
||||
$t('BULK_ACTION.UNASSIGN_CONFIRMATION_LABEL', {
|
||||
conversationCount,
|
||||
conversationLabel,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<div class="agent-confirmation-actions">
|
||||
<woot-button
|
||||
|
@ -82,7 +91,7 @@
|
|||
:is-loading="uiFlags.isUpdating"
|
||||
@click="submit"
|
||||
>
|
||||
{{ $t('BULK_ACTION.ASSIGN_LABEL') }}
|
||||
{{ $t('BULK_ACTION.YES') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -131,7 +140,17 @@ export default {
|
|||
agent.name.toLowerCase().includes(this.query.toLowerCase())
|
||||
);
|
||||
}
|
||||
return this.assignableAgents;
|
||||
return [
|
||||
{
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: null,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
},
|
||||
...this.assignableAgents,
|
||||
];
|
||||
},
|
||||
assignableAgents() {
|
||||
return this.$store.getters['inboxAssignableAgents/getAssignableAgents'](
|
||||
|
|
|
@ -181,7 +181,7 @@ export default {
|
|||
color: var(--y-700);
|
||||
font-size: var(--font-size-mini);
|
||||
margin-top: var(--space-small);
|
||||
padding: var(--space-half) var(--space-one);
|
||||
padding: var(--space-smaller) var(--space-small);
|
||||
}
|
||||
|
||||
.popover-animation-enter-active,
|
||||
|
|
13
app/javascript/dashboard/featureFlags.js
Normal file
13
app/javascript/dashboard/featureFlags.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const FEATURE_FLAGS = {
|
||||
AGENT_BOTS: 'agent_bots',
|
||||
AGENT_MANAGEMENT: 'agent_management',
|
||||
AUTOMATIONS: 'automations',
|
||||
CANNED_RESPONSES: 'canned_responses',
|
||||
CUSTOM_ATTRIBUTES: 'custom_attributes',
|
||||
INBOX_MANAGEMENT: 'inbox_management',
|
||||
INTEGRATIONS: 'integrations',
|
||||
LABELS: 'labels',
|
||||
MACROS: 'macros',
|
||||
TEAM_MANAGEMENT: 'team_management',
|
||||
VOICE_RECORDER: 'voice_recorder',
|
||||
};
|
|
@ -25,6 +25,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
'notification.created': this.onNotificationCreated,
|
||||
'first.reply.created': this.onFirstReplyCreated,
|
||||
'conversation.read': this.onConversationRead,
|
||||
'conversation.updated': this.onConversationUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -67,8 +68,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
};
|
||||
|
||||
onConversationRead = data => {
|
||||
const { contact_last_seen_at: lastSeen } = data;
|
||||
this.app.$store.dispatch('updateConversationRead', lastSeen);
|
||||
this.app.$store.dispatch('updateConversation', data);
|
||||
};
|
||||
|
||||
onLogout = () => AuthAPI.logout();
|
||||
|
@ -85,6 +85,11 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
this.fetchConversationStats();
|
||||
};
|
||||
|
||||
onConversationUpdated = data => {
|
||||
this.app.$store.dispatch('updateConversation', data);
|
||||
this.fetchConversationStats();
|
||||
};
|
||||
|
||||
onTypingOn = ({ conversation, user }) => {
|
||||
const conversationId = conversation.id;
|
||||
|
||||
|
|
94
app/javascript/dashboard/helper/specs/macrosFixtures.js
Normal file
94
app/javascript/dashboard/helper/specs/macrosFixtures.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
export const teams = [
|
||||
{
|
||||
id: 1,
|
||||
name: '⚙️ sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '🤷♂️ fayaz',
|
||||
description: 'Test',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '🇮🇳 apac sales',
|
||||
description: 'Sales team for France Territory',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const labels = [
|
||||
{
|
||||
id: 6,
|
||||
title: 'sales',
|
||||
description: 'sales team',
|
||||
color: '#8EA20F',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'billing',
|
||||
description: 'billing',
|
||||
color: '#4077DA',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'snoozed',
|
||||
description: 'Items marked for later',
|
||||
color: '#D12F42',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'mobile-app',
|
||||
description: 'tech team',
|
||||
color: '#2DB1CC',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
title: 'human-resources-department-with-long-title',
|
||||
description: 'Test',
|
||||
color: '#FF6E09',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
title: 'priority',
|
||||
description: 'For important sales leads',
|
||||
color: '#7E7CED',
|
||||
show_on_sidebar: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const files = [
|
||||
{
|
||||
id: 76,
|
||||
macro_id: 77,
|
||||
file_type: 'image/jpeg',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBYUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--aa41b5a779a83c1d86b28475a5cf0bd17f41f0ff/fayaz_cropped.jpeg',
|
||||
blob_id: 88,
|
||||
filename: 'fayaz_cropped.jpeg',
|
||||
},
|
||||
{
|
||||
id: 82,
|
||||
macro_id: 77,
|
||||
file_type: 'image/png',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBZdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--260fda80b77409ffaaac10b96681fba447600545/screenshot.png',
|
||||
blob_id: 94,
|
||||
filename: 'screenshot.png',
|
||||
},
|
||||
];
|
67
app/javascript/dashboard/helper/specs/macrosHelper.spec.js
Normal file
67
app/javascript/dashboard/helper/specs/macrosHelper.spec.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
emptyMacro,
|
||||
resolveActionName,
|
||||
resolveLabels,
|
||||
resolveTeamIds,
|
||||
getFileName,
|
||||
} from '../../routes/dashboard/settings/macros/macroHelper';
|
||||
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
|
||||
import { teams, labels, files } from './macrosFixtures';
|
||||
|
||||
describe('#emptyMacro', () => {
|
||||
const defaultMacro = {
|
||||
name: '',
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
visibility: 'global',
|
||||
};
|
||||
it('returns the default macro', () => {
|
||||
expect(emptyMacro).toEqual(defaultMacro);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveActionName', () => {
|
||||
it('resolve action name from key and return the correct label', () => {
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[0].key)).toEqual(
|
||||
MACRO_ACTION_TYPES[0].label
|
||||
);
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).toEqual(
|
||||
MACRO_ACTION_TYPES[1].label
|
||||
);
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
|
||||
MACRO_ACTION_TYPES[0].label
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveTeamIds', () => {
|
||||
it('resolves team names from ids, and returns a joined string', () => {
|
||||
const resolvedTeams = '⚙️ sales team, 🤷♂️ fayaz';
|
||||
expect(resolveTeamIds(teams, [1, 2])).toEqual(resolvedTeams);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resolveLabels', () => {
|
||||
it('resolves labels names from ids and returns a joined string', () => {
|
||||
const resolvedLabels = 'sales, billing';
|
||||
expect(resolveLabels(labels, ['sales', 'billing'])).toEqual(resolvedLabels);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFileName', () => {
|
||||
it('returns the correct file name from the list of files', () => {
|
||||
expect(getFileName(files[0].blob_id, 'send_attachment', files)).toEqual(
|
||||
files[0].filename
|
||||
);
|
||||
expect(getFileName(files[1].blob_id, 'send_attachment', files)).toEqual(
|
||||
files[1].filename
|
||||
);
|
||||
expect(getFileName(files[0].blob_id, 'wrong_action', files)).toEqual('');
|
||||
expect(getFileName(null, 'send_attachment', files)).toEqual('');
|
||||
expect(getFileName(files[0].blob_id, 'send_attachment', [])).toEqual('');
|
||||
});
|
||||
});
|
5
app/javascript/dashboard/i18n/locale/ar/agentBots.json
Normal file
5
app/javascript/dashboard/i18n/locale/ar/agentBots.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots"
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
|
||||
"AGENT_SELECT_LABEL": "اختر وكيل",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"GO_BACK_LABEL": "العودة للخلف",
|
||||
"ASSIGN_LABEL": "تكليف",
|
||||
"YES": "نعم",
|
||||
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
|
||||
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
|
||||
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
|
||||
|
|
|
@ -306,7 +306,7 @@
|
|||
"PUBLISH_ARTICLE": {
|
||||
"API": {
|
||||
"ERROR": "حدث خطأ أثناء نشر المقالة",
|
||||
"SUCCESS": "تم نشر المقالة بنجاح"
|
||||
"SUCCESS": "Article published successfully"
|
||||
}
|
||||
},
|
||||
"ARCHIVE_ARTICLE": {
|
||||
|
|
|
@ -239,7 +239,9 @@
|
|||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "عنوان Callback URL",
|
||||
"SUBTITLE": "يجب عليك تكوين URL webhook في بوابة مطور فيسبوك مع عنوان URL المذكور هنا."
|
||||
"SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
|
||||
"WEBHOOK_URL": "رابط Webhook",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"SUBMIT_BUTTON": "إنشاء قناة واتساب",
|
||||
"API": {
|
||||
|
@ -357,7 +359,7 @@
|
|||
},
|
||||
"FINISH": {
|
||||
"TITLE": "أصبحت قناة التواصل جاهزة الآن!",
|
||||
"MESSAGE": "يمكنك الآن التواصل مع عملائك من خلال قناتك الجديدة ",
|
||||
"MESSAGE": "يمكنك الآن التواصل مع عملائك من خلال قناتك الجديدة",
|
||||
"BUTTON_TEXT": "خذني إلى هناك",
|
||||
"MORE_SETTINGS": "المزيد من الإعدادات",
|
||||
"WEBSITE_SUCCESS": "لقد انتهيت بنجاح من إنشاء قناة دردشة مباشرة لموقعك. انسخ الرمز الموضح أدناه وقم بإضافته إلى موقع الويب الخاص بك. في المرة القادمة التي يستخدم فيها العميل الدردشة المباشرة، ستظهر المحادثة تلقائياً على صندوق الوارد الخاص بك."
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/ar/macros.json
Normal file
5
app/javascript/dashboard/i18n/locale/ar/macros.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros"
|
||||
}
|
||||
}
|
|
@ -179,6 +179,7 @@
|
|||
"CONTACTS": "جهات الاتصال",
|
||||
"HOME": "الرئيسية",
|
||||
"AGENTS": "موظف الدعم",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"INBOXES": "قنوات التواصل",
|
||||
"NOTIFICATIONS": "الإشعارات",
|
||||
"CANNED_RESPONSES": "الردود السريعة",
|
||||
|
@ -189,6 +190,7 @@
|
|||
"LABELS": "الوسوم",
|
||||
"CUSTOM_ATTRIBUTES": "سمات مخصصة",
|
||||
"AUTOMATION": "الأتمتة",
|
||||
"MACROS": "Macros",
|
||||
"TEAMS": "الفرق",
|
||||
"BILLING": "الفواتير",
|
||||
"CUSTOM_VIEWS_FOLDER": "المجلدات",
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/bg/agentBots.json
Normal file
5
app/javascript/dashboard/i18n/locale/bg/agentBots.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots"
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Select Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assign",
|
||||
"YES": "Yes",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
|
|
|
@ -306,7 +306,7 @@
|
|||
"PUBLISH_ARTICLE": {
|
||||
"API": {
|
||||
"ERROR": "Error while publishing article",
|
||||
"SUCCESS": "Article publishied successfully"
|
||||
"SUCCESS": "Article published successfully"
|
||||
}
|
||||
},
|
||||
"ARCHIVE_ARTICLE": {
|
||||
|
|
|
@ -239,7 +239,9 @@
|
|||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here."
|
||||
"SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
|
||||
"WEBHOOK_URL": "Webhook URL",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
|
@ -357,7 +359,7 @@
|
|||
},
|
||||
"FINISH": {
|
||||
"TITLE": "Your Inbox is ready!",
|
||||
"MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting ",
|
||||
"MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting",
|
||||
"BUTTON_TEXT": "Take me there",
|
||||
"MORE_SETTINGS": "More settings",
|
||||
"WEBSITE_SUCCESS": "You have successfully finished creating a website channel. Copy the code shown below and paste it on your website. Next time a customer use the live chat, the conversation will automatically appear on your inbox."
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/bg/macros.json
Normal file
5
app/javascript/dashboard/i18n/locale/bg/macros.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros"
|
||||
}
|
||||
}
|
|
@ -179,6 +179,7 @@
|
|||
"CONTACTS": "Контакти",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Агенти",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"INBOXES": "Inboxes",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"CANNED_RESPONSES": "Готови отговори",
|
||||
|
@ -189,6 +190,7 @@
|
|||
"LABELS": "Labels",
|
||||
"CUSTOM_ATTRIBUTES": "Персонализирани атрибути",
|
||||
"AUTOMATION": "Автоматизация",
|
||||
"MACROS": "Macros",
|
||||
"TEAMS": "Teams",
|
||||
"BILLING": "Billing",
|
||||
"CUSTOM_VIEWS_FOLDER": "Folders",
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/ca/agentBots.json
Normal file
5
app/javascript/dashboard/i18n/locale/ca/agentBots.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots"
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Seleccionar Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assignar",
|
||||
"YES": "Si",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
|
|
|
@ -306,7 +306,7 @@
|
|||
"PUBLISH_ARTICLE": {
|
||||
"API": {
|
||||
"ERROR": "Error while publishing article",
|
||||
"SUCCESS": "Article publishied successfully"
|
||||
"SUCCESS": "Article published successfully"
|
||||
}
|
||||
},
|
||||
"ARCHIVE_ARTICLE": {
|
||||
|
|
|
@ -239,7 +239,9 @@
|
|||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here."
|
||||
"SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
|
||||
"WEBHOOK_URL": "URL del webhook",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
|
@ -357,7 +359,7 @@
|
|||
},
|
||||
"FINISH": {
|
||||
"TITLE": "La vostra safata d'entrada està a punt!",
|
||||
"MESSAGE": "Ja podeu interactuar amb els vostres clients a través del vostre canal nou. Feliç suport ",
|
||||
"MESSAGE": "Ja podeu interactuar amb els vostres clients a través del vostre canal nou. Feliç suport",
|
||||
"BUTTON_TEXT": "Porta'm allà",
|
||||
"MORE_SETTINGS": "More settings",
|
||||
"WEBSITE_SUCCESS": "Heu finalitzat amb èxit la creació d'un canal web. Copieu el codi que es mostra a continuació i enganxeu-lo al lloc web. La propera vegada que un client utilitzi el xat en directe, la conversa apareixerà automàticament a la safata d'entrada."
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/ca/macros.json
Normal file
5
app/javascript/dashboard/i18n/locale/ca/macros.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros"
|
||||
}
|
||||
}
|
|
@ -179,6 +179,7 @@
|
|||
"CONTACTS": "Contactes",
|
||||
"HOME": "Inici",
|
||||
"AGENTS": "Agents",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"INBOXES": "Safates d'entrada",
|
||||
"NOTIFICATIONS": "Notificacions",
|
||||
"CANNED_RESPONSES": "Respostes predeterminades",
|
||||
|
@ -189,6 +190,7 @@
|
|||
"LABELS": "Etiquetes",
|
||||
"CUSTOM_ATTRIBUTES": "Atributs personalitzats",
|
||||
"AUTOMATION": "Automation",
|
||||
"MACROS": "Macros",
|
||||
"TEAMS": "Equips",
|
||||
"BILLING": "Billing",
|
||||
"CUSTOM_VIEWS_FOLDER": "Folders",
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/cs/agentBots.json
Normal file
5
app/javascript/dashboard/i18n/locale/cs/agentBots.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots"
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Vybrat agenta",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Přiřadit",
|
||||
"YES": "Ano",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||
|
|
|
@ -306,7 +306,7 @@
|
|||
"PUBLISH_ARTICLE": {
|
||||
"API": {
|
||||
"ERROR": "Error while publishing article",
|
||||
"SUCCESS": "Article publishied successfully"
|
||||
"SUCCESS": "Article published successfully"
|
||||
}
|
||||
},
|
||||
"ARCHIVE_ARTICLE": {
|
||||
|
|
|
@ -239,7 +239,9 @@
|
|||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the webhook URL in facebook developer portal with the URL mentioned here."
|
||||
"SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
|
||||
"WEBHOOK_URL": "URL webového háčku",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
|
@ -357,7 +359,7 @@
|
|||
},
|
||||
"FINISH": {
|
||||
"TITLE": "Vaše doručená pošta je připravena!",
|
||||
"MESSAGE": "Nyní se můžete spojit se svými zákazníky prostřednictvím nového kanálu. Šťastná podpora ",
|
||||
"MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting",
|
||||
"BUTTON_TEXT": "Vezmi mě tam",
|
||||
"MORE_SETTINGS": "More settings",
|
||||
"WEBSITE_SUCCESS": "Úspěšně jste dokončili vytvoření webového kanálu. Zkopírujte kód zobrazený níže a vložte jej na vaše webové stránky. Když zákazník příště použije živý chat, konverzace se automaticky objeví ve vaší doručené poště."
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/cs/macros.json
Normal file
5
app/javascript/dashboard/i18n/locale/cs/macros.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros"
|
||||
}
|
||||
}
|
|
@ -179,6 +179,7 @@
|
|||
"CONTACTS": "Kontakty",
|
||||
"HOME": "Domů",
|
||||
"AGENTS": "Agenti",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"INBOXES": "Schránky",
|
||||
"NOTIFICATIONS": "Oznámení",
|
||||
"CANNED_RESPONSES": "Konzervované odpovědi",
|
||||
|
@ -189,6 +190,7 @@
|
|||
"LABELS": "Štítky",
|
||||
"CUSTOM_ATTRIBUTES": "Vlastní atributy",
|
||||
"AUTOMATION": "Automation",
|
||||
"MACROS": "Macros",
|
||||
"TEAMS": "Týmy",
|
||||
"BILLING": "Billing",
|
||||
"CUSTOM_VIEWS_FOLDER": "Folders",
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/da/agentBots.json
Normal file
5
app/javascript/dashboard/i18n/locale/da/agentBots.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots"
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} samtaler valgt",
|
||||
"AGENT_SELECT_LABEL": "Vælg Agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Er du sikker på, at du vil tildele %{conversationCount} %{conversationLabel} til",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Gå tilbage",
|
||||
"ASSIGN_LABEL": "Tildel",
|
||||
"YES": "Ja",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Tildel Agent",
|
||||
"ASSIGN_SUCCESFUL": "Samtaler tildelt",
|
||||
"ASSIGN_FAILED": "Mislykkedes at tildele samtaler, prøv igen",
|
||||
|
|
|
@ -306,7 +306,7 @@
|
|||
"PUBLISH_ARTICLE": {
|
||||
"API": {
|
||||
"ERROR": "Fejl under publicering af artikel",
|
||||
"SUCCESS": "Artikel publiceret med succes"
|
||||
"SUCCESS": "Article published successfully"
|
||||
}
|
||||
},
|
||||
"ARCHIVE_ARTICLE": {
|
||||
|
|
|
@ -239,7 +239,9 @@
|
|||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "Du skal konfigurere webhook URL i facebook udvikler portal med den URL, der er nævnt her."
|
||||
"SUBTITLE": "You have to configure the webhook URL and the verification token in the Facebook Developer portal with the values shown below.",
|
||||
"WEBHOOK_URL": "Webhook URL",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Opret WhatsApp Kanal",
|
||||
"API": {
|
||||
|
@ -357,7 +359,7 @@
|
|||
},
|
||||
"FINISH": {
|
||||
"TITLE": "Din indbakke er klar!",
|
||||
"MESSAGE": "Du kan nu engagere dig med dine kunder gennem din nye kanal. Glædelig supportering ",
|
||||
"MESSAGE": "Du kan nu engagere dig med dine kunder gennem din nye kanal. Glædelig supportering",
|
||||
"BUTTON_TEXT": "Tag mig med dertil",
|
||||
"MORE_SETTINGS": "Flere indstillinger",
|
||||
"WEBSITE_SUCCESS": "Du er færdig med at oprette en hjemmeside kanal. Kopier koden vist nedenfor og indsæt den på din hjemmeside. Næste gang en kunde bruger live chat, vil samtalen automatisk vises i din indbakke."
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/da/macros.json
Normal file
5
app/javascript/dashboard/i18n/locale/da/macros.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MACROS": {
|
||||
"HEADER": "Macros"
|
||||
}
|
||||
}
|
|
@ -179,6 +179,7 @@
|
|||
"CONTACTS": "Kontakter",
|
||||
"HOME": "Hjem",
|
||||
"AGENTS": "Agenter",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"INBOXES": "Indbakker",
|
||||
"NOTIFICATIONS": "Notifikationer",
|
||||
"CANNED_RESPONSES": "Standardsvar Svar",
|
||||
|
@ -189,6 +190,7 @@
|
|||
"LABELS": "Etiketter",
|
||||
"CUSTOM_ATTRIBUTES": "Brugerdefinerede Egenskaber",
|
||||
"AUTOMATION": "Automatisering",
|
||||
"MACROS": "Macros",
|
||||
"TEAMS": "Teams",
|
||||
"BILLING": "Fakturering",
|
||||
"CUSTOM_VIEWS_FOLDER": "Mapper",
|
||||
|
|
5
app/javascript/dashboard/i18n/locale/de/agentBots.json
Normal file
5
app/javascript/dashboard/i18n/locale/de/agentBots.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots"
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "%{conversationCount} Konversationen ausgewählt",
|
||||
"AGENT_SELECT_LABEL": "Agent auswählen",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Sind Sie sicher, dass Sie %{conversationCount} %{conversationLabel} zuweisen möchten",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Sind Sie sicher, %{conversationCount} %{conversationLabel} zuzuweisen",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Möchten Sie die Zuweisung von %{conversationCount} %{conversationLabel} wirklich aufheben?",
|
||||
"GO_BACK_LABEL": "Zurück",
|
||||
"ASSIGN_LABEL": "Zuordnen",
|
||||
"YES": "Ja",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Agent zuweisen",
|
||||
"ASSIGN_SUCCESFUL": "Konversationen erfolgreich zugewiesen",
|
||||
"ASSIGN_FAILED": "Konversationen konnten nicht zugewiesen werden. Bitte versuchen Sie es erneut",
|
||||
|
|
|
@ -239,7 +239,9 @@
|
|||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "Sie müssen die Webhook-URL im Facebook-Entwicklerportal mit der hier genannten URL konfigurieren."
|
||||
"SUBTITLE": "Sie müssen die Webhook-URL und das Verifizierungstoken im Facebook-Entwicklerportal mit den unten gezeigten Werten konfigurieren.",
|
||||
"WEBHOOK_URL": "Webhook-URL",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook-Verifizierungstoken"
|
||||
},
|
||||
"SUBMIT_BUTTON": "WhatsApp-Kanal erstellen",
|
||||
"API": {
|
||||
|
@ -357,7 +359,7 @@
|
|||
},
|
||||
"FINISH": {
|
||||
"TITLE": "Ihr Posteingang ist fertig!",
|
||||
"MESSAGE": "Sie können jetzt über Ihren neuen Kanal mit Ihren Kunden in Kontakt treten. Viel Spaß beim Unterstützen",
|
||||
"MESSAGE": "Sie können jetzt über Ihren neuen Kanal mit Ihren Kunden in Kontakt treten. Fröhliches Unterstützen",
|
||||
"BUTTON_TEXT": "Bring mich dahin",
|
||||
"MORE_SETTINGS": "Weitere Einstellungen",
|
||||
"WEBSITE_SUCCESS": "Sie haben die Erstellung eines Website-Kanals erfolgreich abgeschlossen. Kopieren Sie den unten gezeigten Code und fügen Sie ihn in Ihre Website ein. Wenn ein Kunde das nächste Mal den Live-Chat verwendet, wird die Konversation automatisch in Ihrem Posteingang angezeigt."
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue