Merge branch 'release/2.4.0'
This commit is contained in:
commit
480eb3043c
358 changed files with 15327 additions and 1703 deletions
|
@ -12,8 +12,8 @@ defaults: &defaults
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||||
- image: circleci/postgres:alpine
|
- image: cimg/postgres:14.1
|
||||||
- image: circleci/redis:alpine
|
- image: cimg/redis:6.2.6
|
||||||
environment:
|
environment:
|
||||||
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
||||||
- RAILS_LOG_TO_STDOUT: false
|
- RAILS_LOG_TO_STDOUT: false
|
||||||
|
@ -110,7 +110,7 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Run backend tests
|
name: Run backend tests
|
||||||
command: |
|
command: |
|
||||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
|
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
|
||||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: ~/tmp
|
root: ~/tmp
|
||||||
|
|
|
@ -32,6 +32,11 @@ REDIS_SENTINELS=
|
||||||
# You can find list of master using "SENTINEL masters" command
|
# You can find list of master using "SENTINEL masters" command
|
||||||
REDIS_SENTINEL_MASTER_NAME=
|
REDIS_SENTINEL_MASTER_NAME=
|
||||||
|
|
||||||
|
# Redis premium breakage in heroku fix
|
||||||
|
# enable the following configuration
|
||||||
|
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||||
|
# REDIS_OPENSSL_VERIFY_MODE=none
|
||||||
|
|
||||||
# Postgres Database config variables
|
# Postgres Database config variables
|
||||||
POSTGRES_HOST=postgres
|
POSTGRES_HOST=postgres
|
||||||
POSTGRES_USERNAME=postgres
|
POSTGRES_USERNAME=postgres
|
||||||
|
|
62
.github/workflows/publish_foss_docker.yml
vendored
Normal file
62
.github/workflows/publish_foss_docker.yml
vendored
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# #
|
||||||
|
# # This action will publish Chatwoot CE docker image.
|
||||||
|
# # This is set to run against merges to develop, master
|
||||||
|
# # and when tags are created.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Publish Chatwoot CE docker images
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Set Chatwoot edition
|
||||||
|
run: |
|
||||||
|
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
|
||||||
|
|
||||||
|
- name: set docker tag
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=chatwoot/chatwoot:$GIT_REF-ce" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: replace docker tag if master
|
||||||
|
if: github.ref_name == 'master'
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.DOCKER_TAG }}
|
72
.github/workflows/run_foss_spec.yml
vendored
Normal file
72
.github/workflows/run_foss_spec.yml
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# #
|
||||||
|
# # This action will strip the enterprise folder
|
||||||
|
# # and run the spec.
|
||||||
|
# # This is set to run against every PR.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Run Chatwoot CE spec
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:10.8
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ""
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
# needed because the postgres container does not provide a healthcheck
|
||||||
|
# tmpfs makes DB faster by using RAM
|
||||||
|
options: >-
|
||||||
|
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.0.2 # Not needed with a .ruby-version file
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- name: yarn
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Create database
|
||||||
|
run: bundle exec rake db:create
|
||||||
|
|
||||||
|
- name: Seed database
|
||||||
|
run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
|
- name: yarn check-files
|
||||||
|
run: yarn install --check-files
|
||||||
|
|
||||||
|
# Run rails tests
|
||||||
|
- name: Run backend tests
|
||||||
|
run: |
|
||||||
|
bundle exec rspec --profile=10 --format documentation
|
|
@ -17,6 +17,7 @@ Metrics/ClassLength:
|
||||||
- 'app/builders/messages/facebook/message_builder.rb'
|
- 'app/builders/messages/facebook/message_builder.rb'
|
||||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||||
- 'app/listeners/action_cable_listener.rb'
|
- 'app/listeners/action_cable_listener.rb'
|
||||||
|
- 'app/models/conversation.rb'
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -125,6 +125,9 @@ gem 'procore-sift'
|
||||||
gem 'email_reply_trimmer'
|
gem 'email_reply_trimmer'
|
||||||
gem 'html2text'
|
gem 'html2text'
|
||||||
|
|
||||||
|
# to calculate working hours
|
||||||
|
gem 'working_hours'
|
||||||
|
|
||||||
group :production, :staging do
|
group :production, :staging do
|
||||||
# we dont want request timing out in development while using byebug
|
# we dont want request timing out in development while using byebug
|
||||||
gem 'rack-timeout'
|
gem 'rack-timeout'
|
||||||
|
|
14
Gemfile.lock
14
Gemfile.lock
|
@ -378,14 +378,14 @@ GEM
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (8.4.0)
|
newrelic_rpm (8.4.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.3)
|
nokogiri (1.13.4)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.3-arm64-darwin)
|
nokogiri (1.13.4-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.3-x86_64-darwin)
|
nokogiri (1.13.4-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.3-x86_64-linux)
|
nokogiri (1.13.4-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.8)
|
oauth (0.5.8)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
@ -403,7 +403,7 @@ GEM
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.6.2)
|
puma (5.6.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -636,6 +636,9 @@ GEM
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
wisper (2.0.0)
|
wisper (2.0.0)
|
||||||
|
working_hours (1.4.1)
|
||||||
|
activesupport (>= 3.2)
|
||||||
|
tzinfo
|
||||||
zeitwerk (2.5.4)
|
zeitwerk (2.5.4)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
@ -746,6 +749,7 @@ DEPENDENCIES
|
||||||
webpacker (~> 5.x)
|
webpacker (~> 5.x)
|
||||||
webpush
|
webpush
|
||||||
wisper (= 2.0.0)
|
wisper (= 2.0.0)
|
||||||
|
working_hours
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.0.2p107
|
ruby 3.0.2p107
|
||||||
|
|
4
app.json
4
app.json
|
@ -32,6 +32,10 @@
|
||||||
"INSTALLATION_ENV": {
|
"INSTALLATION_ENV": {
|
||||||
"description": "Installation method used for Chatwoot.",
|
"description": "Installation method used for Chatwoot.",
|
||||||
"value": "heroku"
|
"value": "heroku"
|
||||||
|
},
|
||||||
|
"REDIS_OPENSSL_VERIFY_MODE":{
|
||||||
|
"description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues",
|
||||||
|
"value": "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"formation": {
|
"formation": {
|
||||||
|
|
|
@ -70,7 +70,7 @@ class ContactBuilder
|
||||||
update_contact_avatar(contact)
|
update_contact_avatar(contact)
|
||||||
contact_inbox
|
contact_inbox
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
ensure_contact_avatar
|
ensure_contact_avatar
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError
|
||||||
Rails.logger.info "Facebook Authorization expired for Inbox #{@inbox.id}"
|
Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
true
|
true
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
class V2::ReportBuilder
|
class V2::ReportBuilder
|
||||||
include DateRangeHelper
|
include DateRangeHelper
|
||||||
|
include ReportHelper
|
||||||
attr_reader :account, :params
|
attr_reader :account, :params
|
||||||
|
|
||||||
DEFAULT_GROUP_BY = 'day'.freeze
|
DEFAULT_GROUP_BY = 'day'.freeze
|
||||||
|
@ -18,8 +19,14 @@ class V2::ReportBuilder
|
||||||
|
|
||||||
# For backward compatible with old report
|
# For backward compatible with old report
|
||||||
def build
|
def build
|
||||||
timeseries.each_with_object([]) do |p, arr|
|
if %w[avg_first_response_time avg_resolution_time].include?(params[:metric])
|
||||||
arr << { value: p[1], timestamp: p[0].to_time.to_i }
|
timeseries.each_with_object([]) do |p, arr|
|
||||||
|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i, count: @grouped_values.count[p[0]] }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
timeseries.each_with_object([]) do |p, arr|
|
||||||
|
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -34,23 +41,16 @@ class V2::ReportBuilder
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def conversation_metrics
|
||||||
|
if params[:type].equal?(:account)
|
||||||
def scope
|
conversations
|
||||||
case params[:type]
|
else
|
||||||
when :account
|
agent_metrics
|
||||||
account
|
|
||||||
when :inbox
|
|
||||||
inbox
|
|
||||||
when :agent
|
|
||||||
user
|
|
||||||
when :label
|
|
||||||
label
|
|
||||||
when :team
|
|
||||||
team
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def inbox
|
def inbox
|
||||||
@inbox ||= account.inboxes.find(params[:id])
|
@inbox ||= account.inboxes.find(params[:id])
|
||||||
end
|
end
|
||||||
|
@ -68,7 +68,7 @@ class V2::ReportBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_grouped_values(object_scope)
|
def get_grouped_values(object_scope)
|
||||||
object_scope.group_by_period(
|
@grouped_values = object_scope.group_by_period(
|
||||||
params[:group_by] || DEFAULT_GROUP_BY,
|
params[:group_by] || DEFAULT_GROUP_BY,
|
||||||
:created_at,
|
:created_at,
|
||||||
default_value: 0,
|
default_value: 0,
|
||||||
|
@ -78,47 +78,26 @@ class V2::ReportBuilder
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations_count
|
def agent_metrics
|
||||||
(get_grouped_values scope.conversations).count
|
users = @account.users
|
||||||
|
users = users.where(id: params[:user_id]) if params[:user_id].present?
|
||||||
|
users.each_with_object([]) do |user, arr|
|
||||||
|
@user = user
|
||||||
|
arr << {
|
||||||
|
user: { id: user.id, name: user.name, thumbnail: user.avatar_url },
|
||||||
|
metric: conversations
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def incoming_messages_count
|
def conversations
|
||||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
@open_conversations = scope.conversations.open
|
||||||
end
|
first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||||
|
metric = {
|
||||||
def outgoing_messages_count
|
open: @open_conversations.count,
|
||||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
unattended: @open_conversations.count - first_response_count
|
||||||
end
|
}
|
||||||
|
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||||
def resolutions_count
|
metric
|
||||||
(get_grouped_values scope.conversations.resolved).count
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_first_response_time
|
|
||||||
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_resolution_time
|
|
||||||
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_resolution_time_summary
|
|
||||||
avg_rt = scope.reporting_events
|
|
||||||
.where(name: 'conversation_resolved', created_at: range)
|
|
||||||
.average(:value)
|
|
||||||
|
|
||||||
return 0 if avg_rt.blank?
|
|
||||||
|
|
||||||
avg_rt
|
|
||||||
end
|
|
||||||
|
|
||||||
def avg_first_response_time_summary
|
|
||||||
avg_frt = scope.reporting_events
|
|
||||||
.where(name: 'first_response', created_at: range)
|
|
||||||
.average(:value)
|
|
||||||
|
|
||||||
return 0 if avg_frt.blank?
|
|
||||||
|
|
||||||
avg_frt
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@agent_bot.destroy
|
@agent_bot.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@agent.current_account_user.destroy
|
@agent.current_account_user.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,28 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@automation_rule = Current.account.automation_rules.create(automation_rules_permit)
|
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||||
|
@automation_rule.actions = params[:actions]
|
||||||
|
|
||||||
|
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
||||||
|
|
||||||
|
@automation_rule.save!
|
||||||
|
process_attachments
|
||||||
|
@automation_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@automation_rule.update(automation_rules_permit)
|
ActiveRecord::Base.transaction do
|
||||||
|
@automation_rule.update!(automation_rules_permit)
|
||||||
|
@automation_rule.actions = params[:actions] if params[:actions]
|
||||||
|
@automation_rule.save!
|
||||||
|
process_attachments
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error e
|
||||||
|
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -30,11 +45,20 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def process_attachments
|
||||||
|
return if params[:attachments].blank?
|
||||||
|
|
||||||
|
params[:attachments].each do |uploaded_attachment|
|
||||||
|
@automation_rule.files.attach(uploaded_attachment)
|
||||||
|
end
|
||||||
|
@automation_rule
|
||||||
|
end
|
||||||
|
|
||||||
def automation_rules_permit
|
def automation_rules_permit
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :description, :event_name, :account_id, :active,
|
:name, :description, :event_name, :account_id, :active,
|
||||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
||||||
actions: [:action_name, { action_params: [{}] }]
|
actions: [:action_name, { action_params: [] }]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
||||||
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
koala = Koala::Facebook::OAuth.new(GlobalConfigService.load('FB_APP_ID', ''), GlobalConfigService.load('FB_APP_SECRET', ''))
|
||||||
koala.exchange_access_token_info(omniauth_token)['access_token']
|
koala.exchange_access_token_info(omniauth_token)['access_token']
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_already_existing_facebook_pages(data)
|
def mark_already_existing_facebook_pages(data)
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@campaign.destroy
|
@campaign.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@canned_response.destroy
|
@canned_response.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@note.destroy
|
@note.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@custom_attribute_definition.destroy
|
@custom_attribute_definition.destroy!
|
||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@custom_filter.destroy
|
@custom_filter.destroy!
|
||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@inbox.destroy
|
@inbox.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@hook.destroy
|
@hook.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@hook.destroy
|
@hook.destroy!
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase:
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@category.destroy
|
@category.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@portal.destroy
|
@portal.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@label.destroy
|
@label.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@team.destroy
|
@team.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@webhook.destroy
|
@webhook.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
|
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
|
||||||
notification_subscription.destroy
|
notification_subscription.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,8 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
browser: browser_params,
|
browser: browser_params,
|
||||||
referer: permitted_params[:message][:referer_url],
|
referer: permitted_params[:message][:referer_url],
|
||||||
initiated_at: timestamp_params
|
initiated_at: timestamp_params
|
||||||
}
|
},
|
||||||
|
custom_attributes: permitted_params[:custom_attributes].presence || {}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -52,16 +53,33 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
mergee_contact: @contact
|
mergee_contact: @contact
|
||||||
).perform
|
).perform
|
||||||
else
|
else
|
||||||
@contact.update!(email: email, name: contact_name)
|
@contact.update!(email: email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_contact_phone_number(phone_number)
|
||||||
|
contact_with_phone_number = @current_account.contacts.find_by(phone_number: phone_number)
|
||||||
|
if contact_with_phone_number
|
||||||
|
@contact = ::ContactMergeAction.new(
|
||||||
|
account: @current_account,
|
||||||
|
base_contact: contact_with_phone_number,
|
||||||
|
mergee_contact: @contact
|
||||||
|
).perform
|
||||||
|
else
|
||||||
|
@contact.update!(phone_number: phone_number)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_email
|
def contact_email
|
||||||
permitted_params[:contact][:email].downcase
|
permitted_params[:contact][:email].downcase if permitted_params[:contact].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_name
|
def contact_name
|
||||||
params[:contact][:name] || contact_email.split('@')[0]
|
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact_phone_number
|
||||||
|
params[:contact][:phone_number]
|
||||||
end
|
end
|
||||||
|
|
||||||
def browser_params
|
def browser_params
|
||||||
|
|
|
@ -7,12 +7,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
process_update_contact
|
||||||
@conversation = create_conversation
|
@conversation = create_conversation
|
||||||
conversation.messages.create(message_params)
|
conversation.messages.create(message_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_update_contact
|
||||||
|
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||||
|
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
|
||||||
|
@contact.update!(name: contact_name) if contact_name.present?
|
||||||
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
head :ok && return if conversation.nil?
|
head :ok && return if conversation.nil?
|
||||||
|
|
||||||
|
@ -45,7 +51,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
head :not_found && return if conversation.nil?
|
return head :not_found if conversation.nil?
|
||||||
|
|
||||||
|
return head :forbidden unless @web_widget.end_conversation?
|
||||||
|
|
||||||
unless conversation.resolved?
|
unless conversation.resolved?
|
||||||
conversation.status = :resolved
|
conversation.status = :resolved
|
||||||
conversation.save
|
conversation.save
|
||||||
|
@ -60,6 +69,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id],
|
||||||
|
custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,41 +35,54 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversations
|
||||||
|
return head :unprocessable_entity if params[:type].blank?
|
||||||
|
|
||||||
|
render json: conversation_metrics
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_summary_params
|
def common_params
|
||||||
{
|
{
|
||||||
type: params[:type].to_sym,
|
type: params[:type].to_sym,
|
||||||
id: params[:id],
|
id: params[:id],
|
||||||
since: range[:current][:since],
|
group_by: params[:group_by],
|
||||||
until: range[:current][:until],
|
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||||
group_by: params[:group_by]
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_summary_params
|
||||||
|
common_params.merge({
|
||||||
|
since: range[:current][:since],
|
||||||
|
until: range[:current][:until]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
def previous_summary_params
|
def previous_summary_params
|
||||||
{
|
common_params.merge({
|
||||||
type: params[:type].to_sym,
|
since: range[:previous][:since],
|
||||||
id: params[:id],
|
until: range[:previous][:until]
|
||||||
since: range[:previous][:since],
|
})
|
||||||
until: range[:previous][:until],
|
|
||||||
group_by: params[:group_by]
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_params
|
def report_params
|
||||||
|
common_params.merge({
|
||||||
|
metric: params[:metric],
|
||||||
|
since: params[:since],
|
||||||
|
until: params[:until],
|
||||||
|
timezone_offset: params[:timezone_offset]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
{
|
{
|
||||||
metric: params[:metric],
|
|
||||||
type: params[:type].to_sym,
|
type: params[:type].to_sym,
|
||||||
since: params[:since],
|
user_id: params[:user_id]
|
||||||
until: params[:until],
|
|
||||||
id: params[:id],
|
|
||||||
group_by: params[:group_by],
|
|
||||||
timezone_offset: params[:timezone_offset]
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -91,4 +104,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
|
||||||
summary
|
summary
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_metrics
|
||||||
|
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ class Platform::Api::V1::AccountUsersController < PlatformController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy
|
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
|
||||||
|
@resource.skip_confirmation!
|
||||||
@resource.save!
|
@resource.save!
|
||||||
@resource.confirm
|
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||||
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info e
|
Rails.logger.error e
|
||||||
redirect_to twitter_app_redirect_url
|
redirect_to twitter_app_redirect_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Webhooks::InstagramController < ApplicationController
|
||||||
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
||||||
render json: :ok
|
render json: :ok
|
||||||
else
|
else
|
||||||
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
|
Rails.logger.warn("Message is not received from the instagram webhook event: #{params['object']}")
|
||||||
head :unprocessable_entity
|
head :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@ class WidgetTestsController < ActionController::Base
|
||||||
before_action :ensure_widget_position
|
before_action :ensure_widget_position
|
||||||
before_action :ensure_widget_type
|
before_action :ensure_widget_type
|
||||||
before_action :ensure_widget_style
|
before_action :ensure_widget_style
|
||||||
|
before_action :ensure_dark_mode
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render
|
render
|
||||||
|
@ -14,6 +15,10 @@ class WidgetTestsController < ActionController::Base
|
||||||
@widget_style = params[:widget_style] || 'standard'
|
@widget_style = params[:widget_style] || 'standard'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_dark_mode
|
||||||
|
@dark_mode = params[:dark_mode] || 'light'
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_widget_position
|
def ensure_widget_position
|
||||||
@widget_position = params[:position] || 'left'
|
@widget_position = params[:position] || 'left'
|
||||||
end
|
end
|
||||||
|
|
15
app/finders/email_channel_finder.rb
Normal file
15
app/finders/email_channel_finder.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
class EmailChannelFinder
|
||||||
|
def initialize(email_object)
|
||||||
|
@email_object = email_object
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
channel = nil
|
||||||
|
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
|
||||||
|
recipient_mails.each do |email|
|
||||||
|
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase)
|
||||||
|
break if channel.present?
|
||||||
|
end
|
||||||
|
channel
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,7 +14,7 @@ module Api::V1::InboxesHelper
|
||||||
Mail.defaults do
|
Mail.defaults do
|
||||||
retriever_method :imap, { address: channel_data[:imap_address],
|
retriever_method :imap, { address: channel_data[:imap_address],
|
||||||
port: channel_data[:imap_port],
|
port: channel_data[:imap_port],
|
||||||
user_name: channel_data[:imap_email],
|
user_name: channel_data[:imap_login],
|
||||||
password: channel_data[:imap_password],
|
password: channel_data[:imap_password],
|
||||||
enable_ssl: channel_data[:imap_enable_ssl] }
|
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||||
end
|
end
|
||||||
|
@ -29,8 +29,12 @@ module Api::V1::InboxesHelper
|
||||||
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
||||||
|
|
||||||
set_smtp_encryption(channel_data, smtp)
|
set_smtp_encryption(channel_data, smtp)
|
||||||
|
check_smtp_connection(channel_data, smtp)
|
||||||
|
end
|
||||||
|
|
||||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_email], channel_data[:smtp_password], :login)
|
def check_smtp_connection(channel_data, smtp)
|
||||||
|
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
||||||
|
channel_data[:smtp_authentication]&.to_sym || :login)
|
||||||
smtp.finish unless smtp&.nil?
|
smtp.finish unless smtp&.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
68
app/helpers/report_helper.rb
Normal file
68
app/helpers/report_helper.rb
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
module ReportHelper
|
||||||
|
private
|
||||||
|
|
||||||
|
def scope
|
||||||
|
case params[:type]
|
||||||
|
when :account
|
||||||
|
account
|
||||||
|
when :inbox
|
||||||
|
inbox
|
||||||
|
when :agent
|
||||||
|
user
|
||||||
|
when :label
|
||||||
|
label
|
||||||
|
when :team
|
||||||
|
team
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversations_count
|
||||||
|
(get_grouped_values scope.conversations).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def incoming_messages_count
|
||||||
|
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def outgoing_messages_count
|
||||||
|
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolutions_count
|
||||||
|
(get_grouped_values scope.conversations.resolved).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_first_response_time
|
||||||
|
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'first_response'))
|
||||||
|
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||||
|
|
||||||
|
grouped_reporting_events.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_resolution_time
|
||||||
|
grouped_reporting_events = (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved'))
|
||||||
|
return grouped_reporting_events.average(:value_in_business_hours) if params[:business_hours]
|
||||||
|
|
||||||
|
grouped_reporting_events.average(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_resolution_time_summary
|
||||||
|
reporting_events = scope.reporting_events
|
||||||
|
.where(name: 'conversation_resolved', created_at: range)
|
||||||
|
avg_rt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||||
|
|
||||||
|
return 0 if avg_rt.blank?
|
||||||
|
|
||||||
|
avg_rt
|
||||||
|
end
|
||||||
|
|
||||||
|
def avg_first_response_time_summary
|
||||||
|
reporting_events = scope.reporting_events
|
||||||
|
.where(name: 'first_response', created_at: range)
|
||||||
|
avg_frt = params[:business_hours] ? reporting_events.average(:value_in_business_hours) : reporting_events.average(:value)
|
||||||
|
|
||||||
|
return 0 if avg_frt.blank?
|
||||||
|
|
||||||
|
avg_frt
|
||||||
|
end
|
||||||
|
end
|
50
app/helpers/reporting_event_helper.rb
Normal file
50
app/helpers/reporting_event_helper.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
module ReportingEventHelper
|
||||||
|
def business_hours(inbox, from, to)
|
||||||
|
return 0 unless inbox.working_hours_enabled?
|
||||||
|
|
||||||
|
inbox_working_hours = configure_working_hours(inbox.working_hours)
|
||||||
|
return 0 if inbox_working_hours.blank?
|
||||||
|
|
||||||
|
# Configure working hours
|
||||||
|
WorkingHours::Config.working_hours = inbox_working_hours
|
||||||
|
|
||||||
|
# Configure timezone
|
||||||
|
WorkingHours::Config.time_zone = inbox.timezone
|
||||||
|
|
||||||
|
# Use inbox timezone to change from & to values.
|
||||||
|
from_in_inbox_timezone = from.in_time_zone(inbox.timezone).to_time
|
||||||
|
to_in_inbox_timezone = to.in_time_zone(inbox.timezone).to_time
|
||||||
|
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def configure_working_hours(working_hours)
|
||||||
|
working_hours.each_with_object({}) do |working_hour, object|
|
||||||
|
object[day(working_hour.day_of_week)] = working_hour_range(working_hour) unless working_hour.closed_all_day?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def day(day_of_week)
|
||||||
|
week_days = {
|
||||||
|
0 => :sun,
|
||||||
|
1 => :mon,
|
||||||
|
2 => :tue,
|
||||||
|
3 => :wed,
|
||||||
|
4 => :thu,
|
||||||
|
5 => :fri,
|
||||||
|
6 => :sat
|
||||||
|
}
|
||||||
|
week_days[day_of_week]
|
||||||
|
end
|
||||||
|
|
||||||
|
def working_hour_range(working_hour)
|
||||||
|
{ format_time(working_hour.open_hour, working_hour.open_minutes) => format_time(working_hour.close_hour, working_hour.close_minutes) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_time(hour, minute)
|
||||||
|
hour = hour < 10 ? "0#{hour}" : hour
|
||||||
|
minute = minute < 10 ? "0#{minute}" : minute
|
||||||
|
"#{hour}:#{minute}"
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" class="app-wrapper app-root">
|
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
|
@ -11,21 +11,28 @@
|
||||||
<woot-snackbar-box />
|
<woot-snackbar-box />
|
||||||
<network-notification />
|
<network-notification />
|
||||||
</div>
|
</div>
|
||||||
|
<loading-state v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { accountIdFromPathname } from './helper/URLHelper';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||||
|
import LoadingState from './components/widgets/LoadingState.vue';
|
||||||
import NetworkNotification from './components/NetworkNotification';
|
import NetworkNotification from './components/NetworkNotification';
|
||||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||||
|
import vueActionCable from './helper/actionCable';
|
||||||
import WootSnackbarBox from './components/SnackbarContainer';
|
import WootSnackbarBox from './components/SnackbarContainer';
|
||||||
|
import {
|
||||||
|
registerSubscription,
|
||||||
|
verifyServiceWorkerExistence,
|
||||||
|
} from './helper/pushHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AddAccountModal,
|
AddAccountModal,
|
||||||
|
LoadingState,
|
||||||
NetworkNotification,
|
NetworkNotification,
|
||||||
UpdateBanner,
|
UpdateBanner,
|
||||||
WootSnackbarBox,
|
WootSnackbarBox,
|
||||||
|
@ -43,13 +50,12 @@ export default {
|
||||||
getAccount: 'accounts/getAccount',
|
getAccount: 'accounts/getAccount',
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
|
authUIFlags: 'getAuthUIFlags',
|
||||||
|
currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
hasAccounts() {
|
hasAccounts() {
|
||||||
return (
|
const { accounts = [] } = this.currentUser || {};
|
||||||
this.currentUser &&
|
return accounts.length > 0;
|
||||||
this.currentUser.accounts &&
|
|
||||||
this.currentUser.accounts.length !== 0
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -58,32 +64,37 @@ export default {
|
||||||
if (!this.hasAccounts) {
|
if (!this.hasAccounts) {
|
||||||
this.showAddAccountModal = true;
|
this.showAddAccountModal = true;
|
||||||
}
|
}
|
||||||
|
verifyServiceWorkerExistence(registration =>
|
||||||
|
registration.pushManager.getSubscription().then(subscription => {
|
||||||
|
if (subscription) {
|
||||||
|
registerSubscription();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
currentAccountId() {
|
||||||
|
if (this.currentAccountId) {
|
||||||
|
this.initializeAccount();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('setUser');
|
|
||||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||||
this.initializeAccount();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setLocale(locale) {
|
setLocale(locale) {
|
||||||
this.$root.$i18n.locale = locale;
|
this.$root.$i18n.locale = locale;
|
||||||
},
|
},
|
||||||
|
|
||||||
async initializeAccount() {
|
async initializeAccount() {
|
||||||
const { pathname } = window.location;
|
await this.$store.dispatch('accounts/get');
|
||||||
const accountId = accountIdFromPathname(pathname);
|
const {
|
||||||
|
locale,
|
||||||
if (accountId) {
|
latest_chatwoot_version: latestChatwootVersion,
|
||||||
await this.$store.dispatch('accounts/get');
|
} = this.getAccount(this.currentAccountId);
|
||||||
const {
|
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||||
locale,
|
this.setLocale(locale);
|
||||||
latest_chatwoot_version: latestChatwootVersion,
|
this.latestChatwootVersion = latestChatwootVersion;
|
||||||
} = this.getAccount(accountId);
|
vueActionCable.init(pubsubToken);
|
||||||
this.setLocale(locale);
|
|
||||||
this.latestChatwootVersion = latestChatwootVersion;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
/* eslint no-console: 0 */
|
|
||||||
/* global axios */
|
/* global axios */
|
||||||
/* eslint no-undef: "error" */
|
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import endPoints from './endPoints';
|
import endPoints from './endPoints';
|
||||||
|
@ -61,41 +59,15 @@ export default {
|
||||||
});
|
});
|
||||||
return fetchPromise;
|
return fetchPromise;
|
||||||
},
|
},
|
||||||
|
hasAuthCookie() {
|
||||||
isLoggedIn() {
|
return !!Cookies.getJSON('cw_d_session_info');
|
||||||
const hasAuthCookie = !!Cookies.getJSON('auth_data');
|
|
||||||
const hasUserCookie = !!Cookies.getJSON('user');
|
|
||||||
return hasAuthCookie && hasUserCookie;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isAdmin() {
|
|
||||||
if (this.isLoggedIn()) {
|
|
||||||
return Cookies.getJSON('user').role === 'administrator';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
getAuthData() {
|
getAuthData() {
|
||||||
if (this.isLoggedIn()) {
|
if (this.hasAuthCookie()) {
|
||||||
return Cookies.getJSON('auth_data');
|
return Cookies.getJSON('cw_d_session_info');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
getPubSubToken() {
|
|
||||||
if (this.isLoggedIn()) {
|
|
||||||
const user = Cookies.getJSON('user') || {};
|
|
||||||
const { pubsub_token: pubsubToken } = user;
|
|
||||||
return pubsubToken;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
getCurrentUser() {
|
|
||||||
if (this.isLoggedIn()) {
|
|
||||||
return Cookies.getJSON('user');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
verifyPasswordToken({ confirmationToken }) {
|
verifyPasswordToken({ confirmationToken }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios
|
axios
|
||||||
|
|
|
@ -8,7 +8,15 @@ class ReportsAPI extends ApiClient {
|
||||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||||
}
|
}
|
||||||
|
|
||||||
getReports(metric, since, until, type = 'account', id, group_by) {
|
getReports(
|
||||||
|
metric,
|
||||||
|
since,
|
||||||
|
until,
|
||||||
|
type = 'account',
|
||||||
|
id,
|
||||||
|
group_by,
|
||||||
|
business_hours
|
||||||
|
) {
|
||||||
return axios.get(`${this.url}`, {
|
return axios.get(`${this.url}`, {
|
||||||
params: {
|
params: {
|
||||||
metric,
|
metric,
|
||||||
|
@ -17,12 +25,13 @@ class ReportsAPI extends ApiClient {
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
group_by,
|
group_by,
|
||||||
|
business_hours,
|
||||||
timezone_offset: getTimeOffset(),
|
timezone_offset: getTimeOffset(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummary(since, until, type = 'account', id, group_by) {
|
getSummary(since, until, type = 'account', id, group_by, business_hours) {
|
||||||
return axios.get(`${this.url}/summary`, {
|
return axios.get(`${this.url}/summary`, {
|
||||||
params: {
|
params: {
|
||||||
since,
|
since,
|
||||||
|
@ -30,6 +39,7 @@ class ReportsAPI extends ApiClient {
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
group_by,
|
group_by,
|
||||||
|
business_hours,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,10 +51,6 @@
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-y-800 {
|
|
||||||
color: var(--y-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-ellipsis {
|
.text-ellipsis {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
|
@ -62,13 +62,13 @@ $default-button-height: 4.0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
@include button-style(var(--y-100), var(--y-200), var(--y-900));
|
@include button-style(var(--y-100), var(--y-200), var(--y-700));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.clear {
|
&.clear {
|
||||||
&.warning {
|
&.warning {
|
||||||
color: var(--y-800);
|
color: var(--y-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.button--only-icon:hover {
|
&.button--only-icon:hover {
|
||||||
|
@ -87,7 +87,7 @@ $default-button-height: 4.0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background: var(--y-100);
|
background: var(--y-50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,13 @@
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
color: $color-heading;
|
color: $color-heading;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: var(--b-400);
|
||||||
|
margin-left: var(--space-micro);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-wrap {
|
.metric-wrap {
|
||||||
|
@ -71,5 +78,10 @@
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-hours {
|
||||||
|
margin: $space-normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,3 +25,21 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-hours {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
margin-bottom: var(--space-normal);
|
||||||
|
margin-left: auto;
|
||||||
|
padding-right: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-hours-text {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
margin-bottom: var(--space-zero);
|
||||||
|
margin-left: var(--space-small);
|
||||||
|
}
|
||||||
|
|
|
@ -15,9 +15,9 @@
|
||||||
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="smooth"
|
||||||
size="small"
|
size="small"
|
||||||
color-scheme="secondary"
|
color-scheme="warning"
|
||||||
icon="dismiss-circle"
|
icon="dismiss-circle"
|
||||||
@click="closeNotification"
|
@click="closeNotification"
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
<p class="sub-head">
|
<p class="sub-head">
|
||||||
{{ subTitle }}
|
{{ subTitle }}
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="note">
|
||||||
|
<span class="note">{{ $t('INBOX_MGMT.NOTE') }}</span>
|
||||||
|
{{ note }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-6 small-12">
|
<div class="medium-6 small-12">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -25,6 +29,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
note: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -46,5 +54,9 @@ export default {
|
||||||
.title--section {
|
.title--section {
|
||||||
padding-right: var(--space-large);
|
padding-right: var(--space-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -57,3 +57,13 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
button:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--w-100);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--w-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||||
getCurrentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
availabilityDisplayLabel() {
|
availabilityDisplayLabel() {
|
||||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||||
|
@ -63,9 +63,6 @@ export default {
|
||||||
availabilityIndex
|
availabilityIndex
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
currentAccountId() {
|
|
||||||
return this.getCurrentAccountId;
|
|
||||||
},
|
|
||||||
currentUserAvailability() {
|
currentUserAvailability() {
|
||||||
return this.getCurrentUserAvailability;
|
return this.getCurrentUserAvailability;
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
:active-menu-item="activePrimaryMenu.key"
|
:active-menu-item="activePrimaryMenu.key"
|
||||||
@toggle-accounts="toggleAccountModal"
|
@toggle-accounts="toggleAccountModal"
|
||||||
@key-shortcut-modal="toggleKeyShortcutModal"
|
@key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
|
@open-notification-panel="openNotificationPanel"
|
||||||
/>
|
/>
|
||||||
<secondary-sidebar
|
<secondary-sidebar
|
||||||
:account-id="accountId"
|
:account-id="accountId"
|
||||||
|
@ -176,6 +177,9 @@ export default {
|
||||||
showAddLabelPopup() {
|
showAddLabelPopup() {
|
||||||
this.$emit('show-add-label-popup');
|
this.$emit('show-add-label-popup');
|
||||||
},
|
},
|
||||||
|
openNotificationPanel() {
|
||||||
|
this.$emit('open-notification-panel');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="notifications-link">
|
<div class="notifications-link">
|
||||||
<primary-nav-item
|
<woot-button
|
||||||
name="NOTIFICATIONS"
|
class-names="notifications-link--button"
|
||||||
icon="alert"
|
variant="clear"
|
||||||
:to="`/app/accounts/${accountId}/notifications`"
|
color-scheme="secondary"
|
||||||
:count="unreadCount"
|
:class="{ 'is-active': isNotificationPanelActive }"
|
||||||
/>
|
@click="openNotificationPanel"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="alert" />
|
||||||
|
<span v-if="unreadCount" class="badge warning">{{ unreadCount }}</span>
|
||||||
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import PrimaryNavItem from './PrimaryNavItem';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { PrimaryNavItem },
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
|
@ -28,8 +30,17 @@ export default {
|
||||||
? `${this.notificationMetadata.unreadCount}`
|
? `${this.notificationMetadata.unreadCount}`
|
||||||
: '99+';
|
: '99+';
|
||||||
},
|
},
|
||||||
|
isNotificationPanelActive() {
|
||||||
|
return this.$route.name === 'notifications_index';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openNotificationPanel() {
|
||||||
|
if (this.$route.name !== 'notifications_index') {
|
||||||
|
this.$emit('open-notification-panel');
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -37,4 +48,32 @@ export default {
|
||||||
.notifications-link {
|
.notifications-link {
|
||||||
margin-bottom: var(--space-small);
|
margin-bottom: var(--space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-minus-smaller);
|
||||||
|
top: var(--space-minus-smaller);
|
||||||
|
}
|
||||||
|
.notifications-link--button {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--s-600);
|
||||||
|
margin: var(--space-small) 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--w-50);
|
||||||
|
color: var(--s-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--w-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: var(--w-50);
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="menu vertical user-menu">
|
<div class="menu vertical user-menu">
|
||||||
<notification-bell />
|
<notification-bell @open-notification-panel="openNotificationPanel" />
|
||||||
<agent-details @toggle-menu="toggleOptions" />
|
<agent-details @toggle-menu="toggleOptions" />
|
||||||
<options-menu
|
<options-menu
|
||||||
:show="showOptionsMenu"
|
:show="showOptionsMenu"
|
||||||
|
@ -83,6 +83,9 @@ export default {
|
||||||
toggleSupportChatWindow() {
|
toggleSupportChatWindow() {
|
||||||
window.$chatwoot.toggle();
|
window.$chatwoot.toggle();
|
||||||
},
|
},
|
||||||
|
openNotificationPanel() {
|
||||||
|
this.$emit('open-notification-panel');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -85,14 +85,20 @@ export default {
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
||||||
toStateName: 'settings_inbox_new',
|
toStateName: 'settings_inbox_new',
|
||||||
newLinkRouteName: 'settings_inbox_new',
|
newLinkRouteName: 'settings_inbox_new',
|
||||||
children: this.inboxes.map(inbox => ({
|
children: this.inboxes
|
||||||
id: inbox.id,
|
.map(inbox => ({
|
||||||
label: inbox.name,
|
id: inbox.id,
|
||||||
truncateLabel: true,
|
label: inbox.name,
|
||||||
toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
|
truncateLabel: true,
|
||||||
type: inbox.channel_type,
|
toState: frontendURL(
|
||||||
phoneNumber: inbox.phone_number,
|
`accounts/${this.accountId}/inbox/${inbox.id}`
|
||||||
})),
|
),
|
||||||
|
type: inbox.channel_type,
|
||||||
|
phoneNumber: inbox.phone_number,
|
||||||
|
}))
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
labelSection() {
|
labelSection() {
|
||||||
|
|
|
@ -14,6 +14,10 @@ const i18nConfig = new VueI18n({
|
||||||
messages: i18n,
|
messages: i18n,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $route = {
|
||||||
|
name: 'notifications_index',
|
||||||
|
};
|
||||||
|
|
||||||
describe('notificationBell', () => {
|
describe('notificationBell', () => {
|
||||||
const accountId = 1;
|
const accountId = 1;
|
||||||
const notificationMetadata = { unreadCount: 19 };
|
const notificationMetadata = { unreadCount: 19 };
|
||||||
|
@ -45,24 +49,40 @@ describe('notificationBell', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should return unread count 19 ', () => {
|
it('it should return unread count 19 ', () => {
|
||||||
const notificationBell = shallowMount(NotificationBell, {
|
const wrapper = shallowMount(NotificationBell, {
|
||||||
store,
|
|
||||||
localVue,
|
localVue,
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
|
store,
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
expect(wrapper.vm.unreadCount).toBe('19');
|
||||||
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
|
|
||||||
expect(statusViewTitle.vm.count).toBe('19');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should return unread count 99+ ', async () => {
|
it('it should return unread count 99+ ', async () => {
|
||||||
notificationMetadata.unreadCount = 101;
|
notificationMetadata.unreadCount = 100;
|
||||||
|
const wrapper = shallowMount(NotificationBell, {
|
||||||
|
localVue,
|
||||||
|
i18n: i18nConfig,
|
||||||
|
store,
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.vm.unreadCount).toBe('99+');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isNotificationPanelActive', async () => {
|
||||||
const notificationBell = shallowMount(NotificationBell, {
|
const notificationBell = shallowMount(NotificationBell, {
|
||||||
store,
|
store,
|
||||||
localVue,
|
localVue,
|
||||||
i18n: i18nConfig,
|
i18n: i18nConfig,
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
|
|
||||||
expect(statusViewTitle.vm.count).toBe('99+');
|
expect(notificationBell.vm.isNotificationPanelActive).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -112,10 +112,10 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background: var(--y-800);
|
background: var(--y-600);
|
||||||
color: var(--s-600);
|
color: var(--y-500);
|
||||||
a {
|
a {
|
||||||
color: var(--s-600);
|
color: var(--y-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ export default {
|
||||||
&.warning {
|
&.warning {
|
||||||
background: var(--y-100);
|
background: var(--y-100);
|
||||||
color: var(--y-900);
|
color: var(--y-900);
|
||||||
border: 1px solid var(--y-300);
|
border: 1px solid var(--y-200);
|
||||||
a {
|
a {
|
||||||
color: var(--y-900);
|
color: var(--y-900);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +1,66 @@
|
||||||
<template>
|
<template>
|
||||||
<label class="switch" :class="classObject">
|
<button
|
||||||
<input
|
type="button"
|
||||||
:id="id"
|
class="toggle-button"
|
||||||
v-model="value"
|
:class="{ active: value }"
|
||||||
class="switch-input"
|
role="switch"
|
||||||
:name="name"
|
:aria-checked="value.toString()"
|
||||||
:disabled="disabled"
|
@click="onClick"
|
||||||
type="checkbox"
|
>
|
||||||
/>
|
<span aria-hidden="true" :class="{ active: value }"></span>
|
||||||
<div class="switch-paddle" :for="name">
|
</button>
|
||||||
<span class="show-for-sr">on off</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
disabled: Boolean,
|
value: { type: Boolean, default: false },
|
||||||
type: { type: String, default: '' },
|
|
||||||
size: { type: String, default: '' },
|
|
||||||
checked: Boolean,
|
|
||||||
name: { type: String, default: '' },
|
|
||||||
id: { type: String, default: '' },
|
|
||||||
},
|
},
|
||||||
data() {
|
methods: {
|
||||||
return {
|
onClick() {
|
||||||
value: null,
|
this.$emit('input', !this.value);
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
classObject() {
|
|
||||||
const { type, size, value } = this;
|
|
||||||
return {
|
|
||||||
[`is-${type}`]: type,
|
|
||||||
[`${size}`]: size,
|
|
||||||
checked: value,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
value(val) {
|
|
||||||
this.$emit('input', val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
beforeMount() {
|
|
||||||
this.value = this.checked;
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$emit('input', (this.value = !!this.checked));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.toggle-button {
|
||||||
|
--toggle-button-box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||||
|
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
||||||
|
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
||||||
|
background-color: var(--s-200);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 19px;
|
||||||
|
position: relative;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-property: background-color;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
width: 34px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--w-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
--space-one-point-five: 1.5rem;
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: 100%;
|
||||||
|
box-shadow: var(--toggle-button-box-shadow);
|
||||||
|
display: inline-block;
|
||||||
|
height: var(--space-one-point-five);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-property: transform;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
width: var(--space-one-point-five);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
transform: translate(var(--space-one-point-five), var(--space-zero));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
<select
|
<select
|
||||||
v-model="action_name"
|
v-model="action_name"
|
||||||
class="action__question"
|
class="action__question"
|
||||||
@change="resetFilter()"
|
:class="{ 'full-width': !showActionInput }"
|
||||||
|
@change="resetAction()"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="attribute in actionTypes"
|
v-for="attribute in actionTypes"
|
||||||
|
@ -17,20 +18,39 @@
|
||||||
{{ attribute.label }}
|
{{ attribute.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="filter__answer--wrap">
|
<div v-if="showActionInput" class="filter__answer--wrap">
|
||||||
<div class="multiselect-wrap--small">
|
<div v-if="inputType">
|
||||||
<multiselect
|
<div
|
||||||
|
v-if="inputType === 'multi_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="action_params"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="'Select'"
|
||||||
|
:multiple="true"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-else-if="inputType === 'email'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
track-by="id"
|
type="email"
|
||||||
label="name"
|
class="answer--text-input"
|
||||||
:placeholder="'Select'"
|
placeholder="Enter email"
|
||||||
:multiple="true"
|
/>
|
||||||
selected-label
|
<input
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
v-else-if="inputType === 'url'"
|
||||||
deselect-label=""
|
v-model="action_params"
|
||||||
:max-height="160"
|
type="url"
|
||||||
:options="dropdownValues"
|
class="answer--text-input"
|
||||||
:allow-empty="false"
|
placeholder="Enter url"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,6 +61,18 @@
|
||||||
@click="removeAction"
|
@click="removeAction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<automation-action-team-message-input
|
||||||
|
v-if="inputType === 'team_message'"
|
||||||
|
v-model="action_params"
|
||||||
|
:teams="dropdownValues"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-if="inputType === 'textarea'"
|
||||||
|
v-model="action_params"
|
||||||
|
rows="4"
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
|
class="action-message"
|
||||||
|
></textarea>
|
||||||
<p
|
<p
|
||||||
v-if="v.action_params.$dirty && v.action_params.$error"
|
v-if="v.action_params.$dirty && v.action_params.$error"
|
||||||
class="filter-error"
|
class="filter-error"
|
||||||
|
@ -51,7 +83,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
AutomationActionTeamMessageInput,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -69,6 +105,10 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null,
|
default: () => null,
|
||||||
},
|
},
|
||||||
|
showActionInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
action_name: {
|
action_name: {
|
||||||
|
@ -91,13 +131,17 @@ export default {
|
||||||
this.$emit('input', { ...payload, action_params: value });
|
this.$emit('input', { ...payload, action_params: value });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
inputType() {
|
||||||
|
return this.actionTypes.find(action => action.key === this.action_name)
|
||||||
|
.inputType;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeAction() {
|
removeAction() {
|
||||||
this.$emit('removeAction');
|
this.$emit('removeAction');
|
||||||
},
|
},
|
||||||
resetFilter() {
|
resetAction() {
|
||||||
this.$emit('resetFilter');
|
this.$emit('resetAction');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -136,6 +180,10 @@ export default {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action__question.full-width {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.filter__answer--wrap {
|
.filter__answer--wrap {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -179,4 +227,7 @@ export default {
|
||||||
.multiselect {
|
.multiselect {
|
||||||
margin-bottom: var(--space-zero);
|
margin-bottom: var(--space-zero);
|
||||||
}
|
}
|
||||||
|
.action-message {
|
||||||
|
margin: var(--space-small) 0 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="multiselect-wrap--small">
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedTeams"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_DROPDOWN_PLACEHOLDER')"
|
||||||
|
:multiple="true"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="teams"
|
||||||
|
:allow-empty="false"
|
||||||
|
@input="updateValue"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-model="message"
|
||||||
|
rows="4"
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
|
@input="updateValue"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
// The value types are dynamic, hence prop validation removed to work with our action schema
|
||||||
|
// eslint-disable-next-line vue/require-prop-types
|
||||||
|
props: ['teams', 'value'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedTeams: [],
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const { team_ids: teamIds } = this.value;
|
||||||
|
this.selectedTeams = teamIds;
|
||||||
|
this.message = this.value.message;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateValue() {
|
||||||
|
this.$emit('input', {
|
||||||
|
team_ids: this.selectedTeams.map(team => team.id),
|
||||||
|
message: this.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.multiselect {
|
||||||
|
margin: var(--space-smaller) var(--space-zero);
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
margin-bottom: var(--space-zero);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,7 +5,14 @@
|
||||||
@click="onClick(index)"
|
@click="onClick(index)"
|
||||||
>
|
>
|
||||||
<h3 class="heading">
|
<h3 class="heading">
|
||||||
{{ heading }}
|
<span>{{ heading }}</span>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="infoText"
|
||||||
|
v-tooltip="infoText"
|
||||||
|
size="14"
|
||||||
|
icon="info"
|
||||||
|
class="info-icon"
|
||||||
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="metric-wrap">
|
<div class="metric-wrap">
|
||||||
<h4 class="metric">
|
<h4 class="metric">
|
||||||
|
@ -22,6 +29,7 @@
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
heading: { type: String, default: '' },
|
heading: { type: String, default: '' },
|
||||||
|
infoText: { type: String, default: '' },
|
||||||
point: { type: [Number, String], default: '' },
|
point: { type: [Number, String], default: '' },
|
||||||
trend: { type: Number, default: null },
|
trend: { type: Number, default: null },
|
||||||
index: { type: Number, default: null },
|
index: { type: Number, default: null },
|
||||||
|
|
|
@ -209,7 +209,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-online-status--busy {
|
.user-online-status--busy {
|
||||||
background: var(--y-700);
|
background: var(--y-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-online-status--offline {
|
.user-online-status--offline {
|
||||||
|
|
|
@ -163,13 +163,13 @@ export default {
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
color: var(--y-800);
|
color: var(--y-700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--note {
|
.button--note {
|
||||||
color: var(--y-900);
|
color: var(--y-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-wrap {
|
.action-wrap {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Bar } from 'vue-chartjs';
|
||||||
const fontFamily =
|
const fontFamily =
|
||||||
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||||
|
|
||||||
const chartOptions = {
|
const defaultChartOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
legend: {
|
legend: {
|
||||||
|
@ -11,10 +11,14 @@ const chartOptions = {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
datasets: {
|
||||||
|
bar: {
|
||||||
|
barPercentage: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [
|
xAxes: [
|
||||||
{
|
{
|
||||||
barPercentage: 1.1,
|
|
||||||
ticks: {
|
ticks: {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
},
|
},
|
||||||
|
@ -39,8 +43,20 @@ const chartOptions = {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: Bar,
|
extends: Bar,
|
||||||
props: ['collection'],
|
props: {
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
chartOptions: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.renderChart(this.collection, chartOptions);
|
this.renderChart(this.collection, {
|
||||||
|
...defaultChartOptions,
|
||||||
|
...this.chartOptions,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
||||||
background: var(--s-500);
|
background: var(--s-500);
|
||||||
}
|
}
|
||||||
&__busy {
|
&__busy {
|
||||||
background: var(--y-700);
|
background: var(--y-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-if="!isHMACVerified"
|
v-if="!isHMACVerified"
|
||||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||||
class="text-y-800"
|
|
||||||
size="14"
|
size="14"
|
||||||
|
class="hmac-warning__icon"
|
||||||
icon="warning"
|
icon="warning"
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -181,7 +181,11 @@ export default {
|
||||||
|
|
||||||
.snoozed--display-text {
|
.snoozed--display-text {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
color: var(--y-900);
|
color: var(--y-600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hmac-warning__icon {
|
||||||
|
color: var(--y-600);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -207,7 +207,11 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
this.formatMessage(this.data.content, this.isATweet) + botMessageContent
|
this.formatMessage(
|
||||||
|
this.data.content,
|
||||||
|
this.isATweet,
|
||||||
|
this.data.private
|
||||||
|
) + botMessageContent
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contentAttributes() {
|
contentAttributes() {
|
||||||
|
|
|
@ -83,7 +83,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<reply-box
|
<reply-box
|
||||||
v-on-clickaway="closePopoutReplyBox"
|
|
||||||
:conversation-id="currentChat.id"
|
:conversation-id="currentChat.id"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
:selected-tweet="selectedTweet"
|
:selected-tweet="selectedTweet"
|
||||||
|
@ -109,7 +108,6 @@ import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
||||||
import { isEscape } from 'shared/helpers/KeyboardHelpers';
|
import { isEscape } from 'shared/helpers/KeyboardHelpers';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -117,7 +115,7 @@ export default {
|
||||||
ReplyBox,
|
ReplyBox,
|
||||||
Banner,
|
Banner,
|
||||||
},
|
},
|
||||||
mixins: [conversationMixin, inboxMixin, eventListenerMixins, clickaway],
|
mixins: [conversationMixin, inboxMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
isContactPanelOpen: {
|
isContactPanelOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="input-group-field">
|
<div class="input-group-field">
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model.trim="$v.ccEmailsVal.$model"
|
v-model.trim="$v.ccEmailsVal.$model"
|
||||||
type="email"
|
type="text"
|
||||||
:class="{ error: $v.ccEmailsVal.$error }"
|
:class="{ error: $v.ccEmailsVal.$error }"
|
||||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
<div class="input-group-field">
|
<div class="input-group-field">
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model.trim="$v.bccEmailsVal.$model"
|
v-model.trim="$v.bccEmailsVal.$model"
|
||||||
type="email"
|
type="text"
|
||||||
:class="{ error: $v.bccEmailsVal.$error }"
|
:class="{ error: $v.bccEmailsVal.$error }"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint no-console: 0 */
|
|
||||||
import Auth from '../api/auth';
|
import Auth from '../api/auth';
|
||||||
|
|
||||||
const parseErrorCode = error => Promise.reject(error);
|
const parseErrorCode = error => Promise.reject(error);
|
||||||
|
@ -7,7 +6,7 @@ export default axios => {
|
||||||
const { apiHost = '' } = window.chatwootConfig || {};
|
const { apiHost = '' } = window.chatwootConfig || {};
|
||||||
const wootApi = axios.create({ baseURL: `${apiHost}/` });
|
const wootApi = axios.create({ baseURL: `${apiHost}/` });
|
||||||
// Add Auth Headers to requests if logged in
|
// Add Auth Headers to requests if logged in
|
||||||
if (Auth.isLoggedIn()) {
|
if (Auth.hasAuthCookie()) {
|
||||||
const {
|
const {
|
||||||
'access-token': accessToken,
|
'access-token': accessToken,
|
||||||
'token-type': tokenType,
|
'token-type': tokenType,
|
||||||
|
|
|
@ -14,6 +14,9 @@ export const getLoginRedirectURL = (ssoAccountId, user) => {
|
||||||
if (ssoAccount) {
|
if (ssoAccount) {
|
||||||
return frontendURL(`accounts/${ssoAccountId}/dashboard`);
|
return frontendURL(`accounts/${ssoAccountId}/dashboard`);
|
||||||
}
|
}
|
||||||
|
if (accounts.length) {
|
||||||
|
return frontendURL(`accounts/${accounts[0].id}/dashboard`);
|
||||||
|
}
|
||||||
return DEFAULT_REDIRECT_URL;
|
return DEFAULT_REDIRECT_URL;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,15 +44,6 @@ export const conversationUrl = ({
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accountIdFromPathname = pathname => {
|
|
||||||
const isInsideAccountScopedURLs = pathname.includes('/app/accounts');
|
|
||||||
const urlParam = pathname.split('/')[3];
|
|
||||||
// eslint-disable-next-line no-restricted-globals
|
|
||||||
const isScoped = isInsideAccountScopedURLs && !isNaN(urlParam);
|
|
||||||
const accountId = isScoped ? Number(urlParam) : '';
|
|
||||||
return accountId;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isValidURL = value => {
|
export const isValidURL = value => {
|
||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
|
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
'contact.deleted': this.onContactDelete,
|
'contact.deleted': this.onContactDelete,
|
||||||
'contact.updated': this.onContactUpdate,
|
'contact.updated': this.onContactUpdate,
|
||||||
'conversation.mentioned': this.onConversationMentioned,
|
'conversation.mentioned': this.onConversationMentioned,
|
||||||
|
'notification.created': this.onNotificationCreated,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,17 +135,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
onContactUpdate = data => {
|
onContactUpdate = data => {
|
||||||
this.app.$store.dispatch('contacts/updateContact', data);
|
this.app.$store.dispatch('contacts/updateContact', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onNotificationCreated = data => {
|
||||||
|
this.app.$store.dispatch('notifications/addNotification', data);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
init() {
|
init(pubsubToken) {
|
||||||
if (AuthAPI.isLoggedIn()) {
|
return new ActionCableConnector(window.WOOT, pubsubToken);
|
||||||
const actionCable = new ActionCableConnector(
|
|
||||||
window.WOOT,
|
|
||||||
AuthAPI.getPubSubToken()
|
|
||||||
);
|
|
||||||
return actionCable;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
|
const formatArray = params => {
|
||||||
|
if (params.length <= 0) {
|
||||||
|
params = [];
|
||||||
|
} else if (params.every(elem => typeof elem === 'string')) {
|
||||||
|
params = [...params];
|
||||||
|
} else {
|
||||||
|
params = params.map(val => val.id);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
const generatePayload = data => {
|
const generatePayload = data => {
|
||||||
const actions = JSON.parse(JSON.stringify(data));
|
const actions = JSON.parse(JSON.stringify(data));
|
||||||
let payload = actions.map(item => {
|
let payload = actions.map(item => {
|
||||||
if (Array.isArray(item.action_params)) {
|
if (Array.isArray(item.action_params)) {
|
||||||
item.action_params = item.action_params.map(val => val.id);
|
item.action_params = formatArray(item.action_params);
|
||||||
} else if (typeof item.values === 'object') {
|
} else if (typeof item.values === 'object') {
|
||||||
item.action_params = [item.action_params.id];
|
item.action_params = [item.action_params.id];
|
||||||
} else if (!item.action_params) {
|
} else if (!item.action_params) {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export const getPushSubscriptionPayload = subscription => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sendRegistrationToServer = subscription => {
|
export const sendRegistrationToServer = subscription => {
|
||||||
if (auth.isLoggedIn()) {
|
if (auth.hasAuthCookie()) {
|
||||||
return NotificationSubscriptions.create(
|
return NotificationSubscriptions.create(
|
||||||
getPushSubscriptionPayload(subscription)
|
getPushSubscriptionPayload(subscription)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
frontendURL,
|
frontendURL,
|
||||||
conversationUrl,
|
conversationUrl,
|
||||||
accountIdFromPathname,
|
|
||||||
isValidURL,
|
isValidURL,
|
||||||
getLoginRedirectURL,
|
getLoginRedirectURL,
|
||||||
} from '../URLHelper';
|
} from '../URLHelper';
|
||||||
|
@ -39,18 +38,6 @@ describe('#URL Helpers', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('accountIdFromPathname', () => {
|
|
||||||
it('should return account id if accont scoped url is passed', () => {
|
|
||||||
expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1);
|
|
||||||
});
|
|
||||||
it('should return empty string if accont scoped url not is passed', () => {
|
|
||||||
expect(accountIdFromPathname('/app/accounts/settings/general')).toBe('');
|
|
||||||
});
|
|
||||||
it('should return empty string if empty string is passed', () => {
|
|
||||||
expect(accountIdFromPathname('')).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isValidURL', () => {
|
describe('isValidURL', () => {
|
||||||
it('should return true if valid url is passed', () => {
|
it('should return true if valid url is passed', () => {
|
||||||
expect(isValidURL('https://chatwoot.com')).toBe(true);
|
expect(isValidURL('https://chatwoot.com')).toBe(true);
|
||||||
|
@ -75,7 +62,7 @@ describe('#URL Helpers', () => {
|
||||||
getLoginRedirectURL('7500', {
|
getLoginRedirectURL('7500', {
|
||||||
accounts: [{ id: '7501', name: 'Test Account 7501' }],
|
accounts: [{ id: '7501', name: 'Test Account 7501' }],
|
||||||
})
|
})
|
||||||
).toBe('/app/');
|
).toBe('/app/accounts/7501/dashboard');
|
||||||
expect(getLoginRedirectURL('7500', null)).toBe('/app/');
|
expect(getLoginRedirectURL('7500', null)).toBe('/app/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"DELETE_MESSAGE": "You need to have atleast one condition to save"
|
"DELETE_MESSAGE": "You need to have atleast one condition to save"
|
||||||
},
|
},
|
||||||
"ACTION": {
|
"ACTION": {
|
||||||
"DELETE_MESSAGE": "You need to have atleast one action to save"
|
"DELETE_MESSAGE": "You need to have atleast one action to save",
|
||||||
|
"TEAM_MESSAGE_INPUT_PLACEHOLDER": "Enter your message here",
|
||||||
|
"TEAM_DROPDOWN_PLACEHOLDER": "Select teams"
|
||||||
},
|
},
|
||||||
"TOGGLE": {
|
"TOGGLE": {
|
||||||
"ACTIVATION_TITLE": "Activate Automation Rule",
|
"ACTIVATION_TITLE": "Activate Automation Rule",
|
||||||
|
|
|
@ -70,6 +70,14 @@
|
||||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||||
"ERROR_MESSAGE": "There was an error, please try again"
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
},
|
},
|
||||||
|
"DELETE_NOTE": {
|
||||||
|
"CONFIRM":{
|
||||||
|
"TITLE": "Confirm Deletion",
|
||||||
|
"MESSAGE": "Are you want sure to delete this note?",
|
||||||
|
"YES": "Yes, Delete it",
|
||||||
|
"NO": "No, Keep it"
|
||||||
|
}
|
||||||
|
},
|
||||||
"DELETE_CONTACT": {
|
"DELETE_CONTACT": {
|
||||||
"BUTTON_LABEL": "Delete Contact",
|
"BUTTON_LABEL": "Delete Contact",
|
||||||
"TITLE": "Delete contact",
|
"TITLE": "Delete contact",
|
||||||
|
|
|
@ -60,6 +60,13 @@
|
||||||
"NOTIFICATIONS_PAGE": {
|
"NOTIFICATIONS_PAGE": {
|
||||||
"HEADER": "Notifications",
|
"HEADER": "Notifications",
|
||||||
"MARK_ALL_DONE": "Mark All Done",
|
"MARK_ALL_DONE": "Mark All Done",
|
||||||
|
"DELETE_TITLE": "deleted",
|
||||||
|
"UNREAD_NOTIFICATION": {
|
||||||
|
"TITLE": "Unread Notifications",
|
||||||
|
"ALL_NOTIFICATIONS": "View all notifications",
|
||||||
|
"LOADING_UNREAD_MESSAGE": "Loading unread notifications...",
|
||||||
|
"EMPTY_MESSAGE": "You have no unread notifications"
|
||||||
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"LOADING_MESSAGE": "Loading notifications...",
|
"LOADING_MESSAGE": "Loading notifications...",
|
||||||
"404": "No Notifications",
|
"404": "No Notifications",
|
||||||
|
|
|
@ -395,7 +395,8 @@
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Features",
|
"LABEL": "Features",
|
||||||
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
|
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
|
||||||
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget"
|
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget",
|
||||||
|
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
|
||||||
},
|
},
|
||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger Script",
|
"MESSENGER_HEADING": "Messenger Script",
|
||||||
|
@ -469,6 +470,7 @@
|
||||||
"IMAP": {
|
"IMAP": {
|
||||||
"TITLE": "IMAP",
|
"TITLE": "IMAP",
|
||||||
"SUBTITLE": "Set your IMAP details",
|
"SUBTITLE": "Set your IMAP details",
|
||||||
|
"NOTE_TEXT": "To enable SMTP, please configure IMAP.",
|
||||||
"UPDATE": "Update IMAP settings",
|
"UPDATE": "Update IMAP settings",
|
||||||
"TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox",
|
"TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox",
|
||||||
"TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
|
"TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
|
||||||
|
@ -484,9 +486,9 @@
|
||||||
"LABEL": "Port",
|
"LABEL": "Port",
|
||||||
"PLACE_HOLDER": "Port"
|
"PLACE_HOLDER": "Port"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"LOGIN": {
|
||||||
"LABEL": "Email",
|
"LABEL": "Login",
|
||||||
"PLACE_HOLDER": "Email"
|
"PLACE_HOLDER": "Login"
|
||||||
},
|
},
|
||||||
"PASSWORD": {
|
"PASSWORD": {
|
||||||
"LABEL": "Password",
|
"LABEL": "Password",
|
||||||
|
@ -512,9 +514,9 @@
|
||||||
"LABEL": "Port",
|
"LABEL": "Port",
|
||||||
"PLACE_HOLDER": "Port"
|
"PLACE_HOLDER": "Port"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"LOGIN": {
|
||||||
"LABEL": "Email",
|
"LABEL": "Login",
|
||||||
"PLACE_HOLDER": "Email"
|
"PLACE_HOLDER": "Login"
|
||||||
},
|
},
|
||||||
"PASSWORD": {
|
"PASSWORD": {
|
||||||
"LABEL": "Password",
|
"LABEL": "Password",
|
||||||
|
@ -527,7 +529,9 @@
|
||||||
"ENCRYPTION": "Encryption",
|
"ENCRYPTION": "Encryption",
|
||||||
"SSL_TLS": "SSL/TLS",
|
"SSL_TLS": "SSL/TLS",
|
||||||
"START_TLS": "STARTTLS",
|
"START_TLS": "STARTTLS",
|
||||||
"OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode"
|
"OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode",
|
||||||
}
|
"AUTH_MECHANISM": "Authentication"
|
||||||
|
},
|
||||||
|
"NOTE": "Note: "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,16 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -76,7 +80,8 @@
|
||||||
{ "id": 2, "groupBy": "Week" },
|
{ "id": 2, "groupBy": "Week" },
|
||||||
{ "id": 3, "groupBy": "Month" },
|
{ "id": 3, "groupBy": "Month" },
|
||||||
{ "id": 4, "groupBy": "Year" }
|
{ "id": 4, "groupBy": "Year" }
|
||||||
]
|
],
|
||||||
|
"BUSINESS_HOURS": "Business Hours"
|
||||||
},
|
},
|
||||||
"AGENT_REPORTS": {
|
"AGENT_REPORTS": {
|
||||||
"HEADER": "Agents Overview",
|
"HEADER": "Agents Overview",
|
||||||
|
@ -98,12 +103,16 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -161,12 +170,16 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -224,12 +237,16 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -287,12 +304,16 @@
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
"NAME": "First response time",
|
"NAME": "First Response Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_TIME": {
|
"RESOLUTION_TIME": {
|
||||||
"NAME": "Resolution Time",
|
"NAME": "Resolution Time",
|
||||||
"DESC": "( Avg )"
|
"DESC": "( Avg )",
|
||||||
|
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||||
|
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_COUNT": {
|
"RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolution Count",
|
||||||
|
@ -361,4 +382,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
"SUCCESS_MESSAGE": "Successfully changed the password",
|
"SUCCESS_MESSAGE": "Successfully changed the password",
|
||||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||||
},
|
},
|
||||||
|
"CAPTCHA": {
|
||||||
|
"ERROR": "Verification expired. Please solve captcha again."
|
||||||
|
},
|
||||||
"SUBMIT": "Submit"
|
"SUBMIT": "Submit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,8 @@
|
||||||
"PASSWORD": {
|
"PASSWORD": {
|
||||||
"LABEL": "Password",
|
"LABEL": "Password",
|
||||||
"PLACEHOLDER": "Password",
|
"PLACEHOLDER": "Password",
|
||||||
"ERROR": "Password is too short"
|
"ERROR": "Password is too short",
|
||||||
|
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character"
|
||||||
},
|
},
|
||||||
"CONFIRM_PASSWORD": {
|
"CONFIRM_PASSWORD": {
|
||||||
"LABEL": "Confirm Password",
|
"LABEL": "Confirm Password",
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
"SELECT_ALL": "select all agents",
|
"SELECT_ALL": "select all agents",
|
||||||
"SELECTED_COUNT": "%{selected} out of %{total} agents selected.",
|
"SELECTED_COUNT": "%{selected} out of %{total} agents selected.",
|
||||||
"BUTTON_TEXT": "Add agents",
|
"BUTTON_TEXT": "Add agents",
|
||||||
"AGENT_VALIDATION_ERROR": "Select atleaset one agent."
|
"AGENT_VALIDATION_ERROR": "Select at least one agent."
|
||||||
},
|
},
|
||||||
|
|
||||||
"FINISH": {
|
"FINISH": {
|
||||||
|
|
|
@ -5,6 +5,7 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountSummary: 'getAccountSummary',
|
accountSummary: 'getAccountSummary',
|
||||||
|
accountReport: 'getAccountReports',
|
||||||
}),
|
}),
|
||||||
calculateTrend() {
|
calculateTrend() {
|
||||||
return metric_key => {
|
return metric_key => {
|
||||||
|
@ -19,15 +20,32 @@ export default {
|
||||||
},
|
},
|
||||||
displayMetric() {
|
displayMetric() {
|
||||||
return metric_key => {
|
return metric_key => {
|
||||||
if (
|
if (this.isAverageMetricType(metric_key)) {
|
||||||
['avg_first_response_time', 'avg_resolution_time'].includes(
|
|
||||||
metric_key
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return formatTime(this.accountSummary[metric_key]);
|
return formatTime(this.accountSummary[metric_key]);
|
||||||
}
|
}
|
||||||
return this.accountSummary[metric_key];
|
return this.accountSummary[metric_key];
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
displayInfoText() {
|
||||||
|
return metric_key => {
|
||||||
|
if (this.metrics[this.currentSelection].KEY !== metric_key) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (this.isAverageMetricType(metric_key)) {
|
||||||
|
const total = this.accountReport.data
|
||||||
|
.map(item => item.count)
|
||||||
|
.reduce((prev, curr) => prev + curr, 0);
|
||||||
|
return `${this.metrics[this.currentSelection].INFO_TEXT} ${total}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isAverageMetricType() {
|
||||||
|
return metric_key => {
|
||||||
|
return ['avg_first_response_time', 'avg_resolution_time'].includes(
|
||||||
|
metric_key
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ describe('reportMixin', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getters = {
|
getters = {
|
||||||
getAccountSummary: () => reportFixtures.summary,
|
getAccountSummary: () => reportFixtures.summary,
|
||||||
|
getAccountReports: () => reportFixtures.report,
|
||||||
};
|
};
|
||||||
store = new Vuex.Store({ getters });
|
store = new Vuex.Store({ getters });
|
||||||
});
|
});
|
||||||
|
@ -24,7 +25,7 @@ describe('reportMixin', () => {
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5);
|
expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5);
|
||||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||||
'3 Min'
|
'3 Min 18 Sec'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,4 +39,67 @@ describe('reportMixin', () => {
|
||||||
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
|
expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
|
||||||
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('display info text', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [reportMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentSelection: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
metrics() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
DESC: '( Avg )',
|
||||||
|
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||||
|
KEY: 'avg_first_response_time',
|
||||||
|
NAME: 'First Response Time',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.displayInfoText('avg_first_response_time')).toEqual(
|
||||||
|
'Total number of conversations used for computation: 4'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('do not display info text', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [reportMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentSelection: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
metrics() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
DESC: '( Total )',
|
||||||
|
INFO_TEXT: '',
|
||||||
|
KEY: 'conversation_count',
|
||||||
|
NAME: 'Conversations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DESC: '( Avg )',
|
||||||
|
INFO_TEXT: 'Total number of conversations used for computation:',
|
||||||
|
KEY: 'avg_first_response_time',
|
||||||
|
NAME: 'First Response Time',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.displayInfoText('conversation_count')).toEqual('');
|
||||||
|
expect(wrapper.vm.displayInfoText('incoming_messages_count')).toEqual('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,4 +15,15 @@ export default {
|
||||||
},
|
},
|
||||||
resolutions_count: 3,
|
resolutions_count: 3,
|
||||||
},
|
},
|
||||||
|
report: {
|
||||||
|
data: [
|
||||||
|
{ value: '0.00', timestamp: 1647541800, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1647628200, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1647714600, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1647801000, count: 0 },
|
||||||
|
{ value: '0.01', timestamp: 1647887400, count: 4 },
|
||||||
|
{ value: '0.00', timestamp: 1647973800, count: 0 },
|
||||||
|
{ value: '0.00', timestamp: 1648060200, count: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,9 +21,19 @@
|
||||||
size="tiny"
|
size="tiny"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
@click="onDelete"
|
@click="toggleDeleteModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<woot-delete-modal
|
||||||
|
v-if="showDeleteModal"
|
||||||
|
:show.sync="showDeleteModal"
|
||||||
|
:on-close="closeDelete"
|
||||||
|
:on-confirm="confirmDeletion"
|
||||||
|
:title="$t('DELETE_NOTE.CONFIRM.TITLE')"
|
||||||
|
:message="$t('DELETE_NOTE.CONFIRM.MESSAGE')"
|
||||||
|
:confirm-text="$t('DELETE_NOTE.CONFIRM.YES')"
|
||||||
|
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="note__content" v-html="formatMessage(note || '')" />
|
<p class="note__content" v-html="formatMessage(note || '')" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +69,11 @@ export default {
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showDeleteModal: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.dynamicTime(this.createdAt);
|
return this.dynamicTime(this.createdAt);
|
||||||
|
@ -73,9 +87,19 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleDeleteModal() {
|
||||||
|
this.showDeleteModal = !this.showDeleteModal;
|
||||||
|
},
|
||||||
onDelete() {
|
onDelete() {
|
||||||
this.$emit('delete', this.id);
|
this.$emit('delete', this.id);
|
||||||
},
|
},
|
||||||
|
confirmDeletion() {
|
||||||
|
this.onDelete();
|
||||||
|
this.closeDelete();
|
||||||
|
},
|
||||||
|
closeDelete() {
|
||||||
|
this.showDeleteModal = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -54,14 +54,9 @@
|
||||||
:class="{ error: $v.credentials.password.$error }"
|
:class="{ error: $v.credentials.password.$error }"
|
||||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||||
:error="
|
:error="passwordErrorText"
|
||||||
$v.credentials.password.$error
|
|
||||||
? $t('SET_NEW_PASSWORD.PASSWORD.ERROR')
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
@blur="$v.credentials.password.$touch"
|
@blur="$v.credentials.password.$touch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model.trim="credentials.confirmPassword"
|
v-model.trim="credentials.confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -77,9 +72,17 @@
|
||||||
/>
|
/>
|
||||||
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
||||||
<vue-hcaptcha
|
<vue-hcaptcha
|
||||||
|
ref="hCaptcha"
|
||||||
|
:class="{ error: !hasAValidCaptcha && didCaptchaReset }"
|
||||||
:sitekey="globalConfig.hCaptchaSiteKey"
|
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||||
@verify="onRecaptchaVerified"
|
@verify="onRecaptchaVerified"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
v-if="!hasAValidCaptcha && didCaptchaReset"
|
||||||
|
class="captcha-error"
|
||||||
|
>
|
||||||
|
{{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
:disabled="isSignupInProgress || !hasAValidCaptcha"
|
:disabled="isSignupInProgress || !hasAValidCaptcha"
|
||||||
|
@ -114,6 +117,7 @@ import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
||||||
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
||||||
|
import { isValidPassword } from 'shared/helpers/Validators';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
VueHcaptcha,
|
VueHcaptcha,
|
||||||
|
@ -129,6 +133,7 @@ export default {
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
hCaptchaClientResponse: '',
|
hCaptchaClientResponse: '',
|
||||||
},
|
},
|
||||||
|
didCaptchaReset: false,
|
||||||
isSignupInProgress: false,
|
isSignupInProgress: false,
|
||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
|
@ -149,6 +154,7 @@ export default {
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
required,
|
required,
|
||||||
|
isValidPassword,
|
||||||
minLength: minLength(6),
|
minLength: minLength(6),
|
||||||
},
|
},
|
||||||
confirmPassword: {
|
confirmPassword: {
|
||||||
|
@ -178,11 +184,25 @@ export default {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
passwordErrorText() {
|
||||||
|
const { password } = this.$v.credentials;
|
||||||
|
if (!password.$error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!password.minLength) {
|
||||||
|
return this.$t('REGISTER.PASSWORD.ERROR');
|
||||||
|
}
|
||||||
|
if (!password.isValidPassword) {
|
||||||
|
return this.$t('REGISTER.PASSWORD.IS_INVALID_PASSWORD');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submit() {
|
async submit() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) {
|
if (this.$v.$invalid) {
|
||||||
|
this.resetCaptcha();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isSignupInProgress = true;
|
this.isSignupInProgress = true;
|
||||||
|
@ -194,6 +214,7 @@ export default {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = this.$t('REGISTER.API.ERROR_MESSAGE');
|
let errorMessage = this.$t('REGISTER.API.ERROR_MESSAGE');
|
||||||
if (error.response && error.response.data.message) {
|
if (error.response && error.response.data.message) {
|
||||||
|
this.resetCaptcha();
|
||||||
errorMessage = error.response.data.message;
|
errorMessage = error.response.data.message;
|
||||||
}
|
}
|
||||||
this.showAlert(errorMessage);
|
this.showAlert(errorMessage);
|
||||||
|
@ -203,6 +224,15 @@ export default {
|
||||||
},
|
},
|
||||||
onRecaptchaVerified(token) {
|
onRecaptchaVerified(token) {
|
||||||
this.credentials.hCaptchaClientResponse = token;
|
this.credentials.hCaptchaClientResponse = token;
|
||||||
|
this.didCaptchaReset = false;
|
||||||
|
},
|
||||||
|
resetCaptcha() {
|
||||||
|
if (!this.globalConfig.hCaptchaSiteKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$refs.hCaptcha.reset();
|
||||||
|
this.credentials.hCaptchaClientResponse = '';
|
||||||
|
this.didCaptchaReset = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -254,6 +284,18 @@ export default {
|
||||||
|
|
||||||
.h-captcha--box {
|
.h-captcha--box {
|
||||||
margin-bottom: var(--space-one);
|
margin-bottom: var(--space-one);
|
||||||
|
|
||||||
|
.captcha-error {
|
||||||
|
color: var(--r-400);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::v-deep .error {
|
||||||
|
iframe {
|
||||||
|
border: 1px solid var(--r-500);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<sidebar
|
<sidebar
|
||||||
:route="currentRoute"
|
:route="currentRoute"
|
||||||
:class="sidebarClassName"
|
:class="sidebarClassName"
|
||||||
|
@open-notification-panel="openNotificationPanel"
|
||||||
@toggle-account-modal="toggleAccountModal"
|
@toggle-account-modal="toggleAccountModal"
|
||||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||||
|
@ -25,6 +26,10 @@
|
||||||
@close="closeKeyShortcutModal"
|
@close="closeKeyShortcutModal"
|
||||||
@clickaway="closeKeyShortcutModal"
|
@clickaway="closeKeyShortcutModal"
|
||||||
/>
|
/>
|
||||||
|
<notification-panel
|
||||||
|
v-if="isNotificationPanel"
|
||||||
|
@close="closeNotificationPanel"
|
||||||
|
/>
|
||||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||||
<add-label-modal @close="hideAddLabelPopup" />
|
<add-label-modal @close="hideAddLabelPopup" />
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
|
@ -40,6 +45,7 @@ import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShor
|
||||||
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal';
|
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
|
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
|
||||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||||
|
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -49,6 +55,7 @@ export default {
|
||||||
AddAccountModal,
|
AddAccountModal,
|
||||||
AccountSelector,
|
AccountSelector,
|
||||||
AddLabelModal,
|
AddLabelModal,
|
||||||
|
NotificationPanel,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -58,6 +65,7 @@ export default {
|
||||||
showCreateAccountModal: false,
|
showCreateAccountModal: false,
|
||||||
showAddLabelModal: false,
|
showAddLabelModal: false,
|
||||||
showShortcutModal: false,
|
showShortcutModal: false,
|
||||||
|
isNotificationPanel: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -84,7 +92,6 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('setCurrentAccountId', this.$route.params.accountId);
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
this.handleResize();
|
this.handleResize();
|
||||||
bus.$on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
bus.$on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||||
|
@ -126,6 +133,12 @@ export default {
|
||||||
hideAddLabelPopup() {
|
hideAddLabelPopup() {
|
||||||
this.showAddLabelModal = false;
|
this.showAddLabelModal = false;
|
||||||
},
|
},
|
||||||
|
openNotificationPanel() {
|
||||||
|
this.isNotificationPanel = true;
|
||||||
|
},
|
||||||
|
closeNotificationPanel() {
|
||||||
|
this.isNotificationPanel = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -59,7 +59,29 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<label :class="{ error: $v.message.$error }">
|
<div v-if="isAnEmailInbox || isAnWebWidgetInbox">
|
||||||
|
<label>
|
||||||
|
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
||||||
|
<reply-email-head
|
||||||
|
v-if="isAnEmailInbox"
|
||||||
|
:cc-emails.sync="ccEmails"
|
||||||
|
:bcc-emails.sync="bccEmails"
|
||||||
|
/>
|
||||||
|
<label class="editor-wrap">
|
||||||
|
<woot-message-editor
|
||||||
|
v-model="message"
|
||||||
|
class="message-editor"
|
||||||
|
:class="{ editor_warning: $v.message.$error }"
|
||||||
|
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
||||||
|
@blur="$v.message.$touch"
|
||||||
|
/>
|
||||||
|
<span v-if="$v.message.$error" class="editor-warning__message">
|
||||||
|
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label v-else :class="{ error: $v.message.$error }">
|
||||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
||||||
<textarea
|
<textarea
|
||||||
v-model="message"
|
v-model="message"
|
||||||
|
@ -89,6 +111,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||||
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||||
|
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead';
|
||||||
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||||
|
@ -98,6 +122,8 @@ import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
WootMessageEditor,
|
||||||
|
ReplyEmailHead,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
mixins: [alertMixin],
|
||||||
props: {
|
props: {
|
||||||
|
@ -116,6 +142,8 @@ export default {
|
||||||
subject: '',
|
subject: '',
|
||||||
message: '',
|
message: '',
|
||||||
selectedInbox: '',
|
selectedInbox: '',
|
||||||
|
bccEmails: '',
|
||||||
|
ccEmails: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
|
@ -136,7 +164,7 @@ export default {
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
}),
|
}),
|
||||||
getNewConversation() {
|
getNewConversation() {
|
||||||
return {
|
const payload = {
|
||||||
inboxId: this.targetInbox.inbox.id,
|
inboxId: this.targetInbox.inbox.id,
|
||||||
sourceId: this.targetInbox.source_id,
|
sourceId: this.targetInbox.source_id,
|
||||||
contactId: this.contact.id,
|
contactId: this.contact.id,
|
||||||
|
@ -144,6 +172,14 @@ export default {
|
||||||
mailSubject: this.subject,
|
mailSubject: this.subject,
|
||||||
assigneeId: this.currentUser.id,
|
assigneeId: this.currentUser.id,
|
||||||
};
|
};
|
||||||
|
if (this.ccEmails) {
|
||||||
|
payload.message.cc_emails = this.ccEmails;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.bccEmails) {
|
||||||
|
payload.message.bcc_emails = this.bccEmails;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
},
|
},
|
||||||
targetInbox: {
|
targetInbox: {
|
||||||
get() {
|
get() {
|
||||||
|
@ -168,6 +204,12 @@ export default {
|
||||||
this.selectedInbox.inbox.channel_type === INBOX_TYPES.EMAIL
|
this.selectedInbox.inbox.channel_type === INBOX_TYPES.EMAIL
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isAnWebWidgetInbox() {
|
||||||
|
return (
|
||||||
|
this.selectedInbox &&
|
||||||
|
this.selectedInbox.inbox.channel_type === INBOX_TYPES.WEB
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onCancel() {
|
onCancel() {
|
||||||
|
|
|
@ -0,0 +1,274 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal-mask">
|
||||||
|
<div
|
||||||
|
v-on-clickaway="closeNotificationPanel"
|
||||||
|
class="notification-wrap flex-space-between"
|
||||||
|
>
|
||||||
|
<div class="header-wrap w-full flex-space-between">
|
||||||
|
<div class="header-title--wrap flex-view">
|
||||||
|
<span class="header-title">
|
||||||
|
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.TITLE') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="totalUnreadNotifications" class="total-count block-title">
|
||||||
|
{{ totalUnreadNotifications }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-view">
|
||||||
|
<woot-button
|
||||||
|
v-if="!noUnreadNotificationAvailable"
|
||||||
|
color-scheme="primary"
|
||||||
|
variant="smooth"
|
||||||
|
size="tiny"
|
||||||
|
class-names="action-button"
|
||||||
|
:is-loading="uiFlags.isUpdating"
|
||||||
|
@click="onMarkAllDoneClick"
|
||||||
|
>
|
||||||
|
{{ $t('NOTIFICATIONS_PAGE.MARK_ALL_DONE') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="link"
|
||||||
|
size="tiny"
|
||||||
|
icon="dismiss"
|
||||||
|
@click="closeNotificationPanel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<notification-panel-list
|
||||||
|
:notifications="getUnreadNotifications"
|
||||||
|
:is-loading="uiFlags.isFetching"
|
||||||
|
:on-click-notification="openConversation"
|
||||||
|
:in-last-page="inLastPage"
|
||||||
|
/>
|
||||||
|
<div v-if="records.length !== 0" class="footer-wrap flex-space-between">
|
||||||
|
<div class="flex-view">
|
||||||
|
<woot-button
|
||||||
|
size="medium"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class-names="page-change--button"
|
||||||
|
:is-disabled="inFirstPage"
|
||||||
|
@click="onClickFirstPage"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="chevron-left" size="16" />
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevron-left"
|
||||||
|
size="16"
|
||||||
|
class="margin-left-minus-slab"
|
||||||
|
/>
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="clear"
|
||||||
|
size="medium"
|
||||||
|
icon="chevron-left"
|
||||||
|
:disabled="inFirstPage"
|
||||||
|
@click="onClickPreviousPage"
|
||||||
|
>
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
<span class="page-count"> {{ currentPage }} - {{ lastPage }} </span>
|
||||||
|
<div class="flex-view">
|
||||||
|
<woot-button
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="clear"
|
||||||
|
size="medium"
|
||||||
|
icon="chevron-right"
|
||||||
|
:disabled="inLastPage"
|
||||||
|
@click="onClickNextPage"
|
||||||
|
>
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
size="medium"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
class-names="page-change--button"
|
||||||
|
:disabled="inLastPage"
|
||||||
|
@click="onClickLastPage"
|
||||||
|
>
|
||||||
|
<fluent-icon icon="chevron-right" size="16" />
|
||||||
|
<fluent-icon
|
||||||
|
icon="chevron-right"
|
||||||
|
size="16"
|
||||||
|
class="margin-left-minus-slab"
|
||||||
|
/>
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
|
||||||
|
import NotificationPanelList from './NotificationPanelList';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
NotificationPanelList,
|
||||||
|
},
|
||||||
|
mixins: [clickaway],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pageSize: 15,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
meta: 'notifications/getMeta',
|
||||||
|
records: 'notifications/getNotifications',
|
||||||
|
uiFlags: 'notifications/getUIFlags',
|
||||||
|
}),
|
||||||
|
totalUnreadNotifications() {
|
||||||
|
return this.meta.unreadCount;
|
||||||
|
},
|
||||||
|
noUnreadNotificationAvailable() {
|
||||||
|
return this.meta.unreadCount === 0;
|
||||||
|
},
|
||||||
|
getUnreadNotifications() {
|
||||||
|
return this.records.filter(notification => notification.read_at === null);
|
||||||
|
},
|
||||||
|
currentPage() {
|
||||||
|
return Number(this.meta.currentPage);
|
||||||
|
},
|
||||||
|
lastPage() {
|
||||||
|
if (this.totalUnreadNotifications > 15) {
|
||||||
|
return Math.ceil(this.totalUnreadNotifications / this.pageSize);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
inFirstPage() {
|
||||||
|
const page = Number(this.meta.currentPage);
|
||||||
|
return page === 1;
|
||||||
|
},
|
||||||
|
inLastPage() {
|
||||||
|
return this.currentPage === this.lastPage;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('notifications/get', { page: 1 });
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onPageChange(page) {
|
||||||
|
this.$store.dispatch('notifications/get', { page });
|
||||||
|
},
|
||||||
|
openConversation(notification) {
|
||||||
|
const {
|
||||||
|
primary_actor_id: primaryActorId,
|
||||||
|
primary_actor_type: primaryActorType,
|
||||||
|
primary_actor: { id: conversationId },
|
||||||
|
} = notification;
|
||||||
|
|
||||||
|
this.$store.dispatch('notifications/read', {
|
||||||
|
primaryActorId,
|
||||||
|
primaryActorType,
|
||||||
|
unreadCount: this.meta.unreadCount,
|
||||||
|
});
|
||||||
|
this.$router.push({
|
||||||
|
name: 'inbox_conversation',
|
||||||
|
params: { conversation_id: conversationId },
|
||||||
|
});
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
onClickNextPage() {
|
||||||
|
if (!this.inLastPage) {
|
||||||
|
const page = this.currentPage + 1;
|
||||||
|
this.onPageChange(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickPreviousPage() {
|
||||||
|
if (!this.inFirstPage) {
|
||||||
|
const page = this.currentPage - 1;
|
||||||
|
this.onPageChange(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickFirstPage() {
|
||||||
|
if (!this.inFirstPage) {
|
||||||
|
const page = 1;
|
||||||
|
this.onPageChange(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickLastPage() {
|
||||||
|
if (!this.inLastPage) {
|
||||||
|
const page = this.lastPage;
|
||||||
|
this.onPageChange(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMarkAllDoneClick() {
|
||||||
|
this.$store.dispatch('notifications/readAll');
|
||||||
|
},
|
||||||
|
closeNotificationPanel() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flex-view {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-space-between {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-wrap {
|
||||||
|
flex-direction: column;
|
||||||
|
height: 90vh;
|
||||||
|
width: 52rem;
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-jumbo);
|
||||||
|
margin: var(--space-small);
|
||||||
|
}
|
||||||
|
.header-wrap {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--s-50);
|
||||||
|
padding: var(--space-two) var(--space-medium) var(--space-slab)
|
||||||
|
var(--space-medium);
|
||||||
|
|
||||||
|
.header-title--wrap {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: var(--font-size-two);
|
||||||
|
font-weight: var(--font-weight-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-count {
|
||||||
|
padding: var(--space-smaller) var(--space-small);
|
||||||
|
background: var(--b-50);
|
||||||
|
border-radius: var(--border-radius-rounded);
|
||||||
|
font-size: var(--font-size-micro);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
padding: var(--space-micro) var(--space-small);
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-count {
|
||||||
|
font-size: var(--font-size-micro);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--s-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-wrap {
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-smaller) var(--space-two);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-change--button:hover {
|
||||||
|
background: var(--s-50);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,219 @@
|
||||||
|
<template>
|
||||||
|
<div class="notification-list-item--wrap h-full flex-view ">
|
||||||
|
<woot-button
|
||||||
|
v-for="notificationItem in notifications"
|
||||||
|
v-show="!isLoading"
|
||||||
|
:key="notificationItem.id"
|
||||||
|
size="expanded"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="link"
|
||||||
|
@click="() => onClickNotification(notificationItem)"
|
||||||
|
>
|
||||||
|
<div class="notification-list--wrap flex-view w-full">
|
||||||
|
<div
|
||||||
|
v-if="!notificationItem.read_at"
|
||||||
|
class="notification-unread--indicator"
|
||||||
|
></div>
|
||||||
|
<div v-else class="empty flex-view"></div>
|
||||||
|
<div class="notification-content--wrap w-full flex-space-between">
|
||||||
|
<div class="flex-space-between">
|
||||||
|
<div class="title-wrap flex-view ">
|
||||||
|
<span class="notification-title">
|
||||||
|
{{
|
||||||
|
`#${
|
||||||
|
notificationItem.primary_actor
|
||||||
|
? notificationItem.primary_actor.id
|
||||||
|
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||||
|
}`
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span class="notification-type">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<thumbnail
|
||||||
|
v-if="notificationItem.primary_actor.meta.assignee"
|
||||||
|
:src="notificationItem.primary_actor.meta.assignee.thumbnail"
|
||||||
|
size="16px"
|
||||||
|
:username="notificationItem.primary_actor.meta.assignee.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex-view ">
|
||||||
|
<span class="notification-message text-truncate">
|
||||||
|
{{ notificationItem.push_message_title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp flex-view">
|
||||||
|
{{ dynamicTime(notificationItem.created_at) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</woot-button>
|
||||||
|
<empty-state
|
||||||
|
v-if="showEmptyResult"
|
||||||
|
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-if="!isLoading && inLastPage"
|
||||||
|
size="medium"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="primary"
|
||||||
|
class-names="action-button"
|
||||||
|
@click="openNotificationPage"
|
||||||
|
>
|
||||||
|
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.ALL_NOTIFICATIONS') }}
|
||||||
|
</woot-button>
|
||||||
|
<div v-if="isLoading" class="notifications-loader flex-view">
|
||||||
|
<spinner />
|
||||||
|
<span>{{
|
||||||
|
$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.LOADING_UNREAD_MESSAGE')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
|
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||||
|
import timeMixin from '../../../../mixins/time';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
Spinner,
|
||||||
|
EmptyState,
|
||||||
|
},
|
||||||
|
mixins: [timeMixin],
|
||||||
|
props: {
|
||||||
|
notifications: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
onClickNotification: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
inLastPage: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
notificationMetadata: 'notifications/getMeta',
|
||||||
|
}),
|
||||||
|
showEmptyResult() {
|
||||||
|
return !this.isLoading && this.notifications.length === 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openNotificationPage() {
|
||||||
|
if (this.$route.name !== 'notifications_index') {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'notifications_index',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flex-view {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-space-between {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list-item--wrap {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--space-small) var(--space-slab);
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
width: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list--wrap {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-slab);
|
||||||
|
line-height: 1.4;
|
||||||
|
border-bottom: 1px solid var(--b-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list--wrap:hover {
|
||||||
|
background: var(--b-100);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content--wrap {
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: var(--space-slab);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
font-weight: var(--font-weight-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-type {
|
||||||
|
font-size: var(--font-size-micro);
|
||||||
|
padding: var(--space-micro) var(--space-smaller);
|
||||||
|
margin-left: var(--space-small);
|
||||||
|
background: var(--s-50);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
color: var(--color-body);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
margin-top: var(--space-smaller);
|
||||||
|
color: var(--b-500);
|
||||||
|
font-size: var(--font-size-micro);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-unread--indicator {
|
||||||
|
width: var(--space-small);
|
||||||
|
height: var(--space-small);
|
||||||
|
border-radius: var(--border-radius-rounded);
|
||||||
|
background: var(--color-woot);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
margin-top: var(--space-slab);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-loader {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: var(--space-larger) var(--space-small);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,17 +17,17 @@
|
||||||
@click="() => onClickNotification(notificationItem)"
|
@click="() => onClickNotification(notificationItem)"
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<div class="">
|
<div class="flex-view notification-contant--wrap">
|
||||||
<h5 class="notification--title">
|
<h5 class="notification--title">
|
||||||
{{
|
{{
|
||||||
`#${
|
`#${
|
||||||
notificationItem.primary_actor
|
notificationItem.primary_actor
|
||||||
? notificationItem.primary_actor.id
|
? notificationItem.primary_actor.id
|
||||||
: 'deleted'
|
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||||
}`
|
}`
|
||||||
}}
|
}}
|
||||||
</h5>
|
</h5>
|
||||||
<span class="notification--message-title">
|
<span class="notification--message-title text-truncate">
|
||||||
{{ notificationItem.push_message_title }}
|
{{ notificationItem.push_message_title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -197,6 +197,11 @@ export default {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-contant--wrap {
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
.notification--message-title {
|
.notification--message-title {
|
||||||
color: var(--s-700);
|
color: var(--s-700);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
</td>
|
</td>
|
||||||
<!-- Agent Name + Email -->
|
<!-- Agent Name + Email -->
|
||||||
<td>
|
<td>
|
||||||
<span class="agent-name">{{ agent.name }}</span>
|
<span class="agent-name">
|
||||||
|
{{ agent.name }}
|
||||||
|
</span>
|
||||||
<span>{{ agent.email }}</span>
|
<span>{{ agent.email }}</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- Agent Role + Verification Status -->
|
<!-- Agent Role + Verification Status -->
|
||||||
|
|
|
@ -100,7 +100,11 @@
|
||||||
:dropdown-values="
|
:dropdown-values="
|
||||||
getActionDropdownValues(automation.actions[i].action_name)
|
getActionDropdownValues(automation.actions[i].action_name)
|
||||||
"
|
"
|
||||||
|
:show-action-input="
|
||||||
|
showActionInput(automation.actions[i].action_name)
|
||||||
|
"
|
||||||
:v="$v.automation.actions.$each[i]"
|
:v="$v.automation.actions.$each[i]"
|
||||||
|
@resetAction="resetAction(i)"
|
||||||
@removeAction="removeAction(i)"
|
@removeAction="removeAction(i)"
|
||||||
/>
|
/>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
|
@ -187,7 +191,14 @@ export default {
|
||||||
required,
|
required,
|
||||||
$each: {
|
$each: {
|
||||||
action_params: {
|
action_params: {
|
||||||
required,
|
required: requiredIf(prop => {
|
||||||
|
if (prop.action_name === 'send_email_to_team') return true;
|
||||||
|
return !(
|
||||||
|
prop.action_name === 'mute_conversation' ||
|
||||||
|
prop.action_name === 'snooze_conversation' ||
|
||||||
|
prop.action_name === 'resolve_conversation'
|
||||||
|
);
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -407,14 +418,15 @@ export default {
|
||||||
submitAutomation() {
|
submitAutomation() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) return;
|
if (this.$v.$invalid) return;
|
||||||
this.automation.conditions[
|
const automation = JSON.parse(JSON.stringify(this.automation));
|
||||||
this.automation.conditions.length - 1
|
automation.conditions[
|
||||||
|
automation.conditions.length - 1
|
||||||
].query_operator = null;
|
].query_operator = null;
|
||||||
this.automation.conditions = filterQueryGenerator(
|
automation.conditions = filterQueryGenerator(
|
||||||
this.automation.conditions
|
automation.conditions
|
||||||
).payload;
|
).payload;
|
||||||
this.automation.actions = actionQueryGenerator(this.automation.actions);
|
automation.actions = actionQueryGenerator(automation.actions);
|
||||||
this.$emit('saveAutomation', this.automation);
|
this.$emit('saveAutomation', automation);
|
||||||
},
|
},
|
||||||
resetFilter(index, currentCondition) {
|
resetFilter(index, currentCondition) {
|
||||||
this.automation.conditions[index].filter_operator = this.automationTypes[
|
this.automation.conditions[index].filter_operator = this.automationTypes[
|
||||||
|
@ -424,11 +436,23 @@ export default {
|
||||||
).filterOperators[0].value;
|
).filterOperators[0].value;
|
||||||
this.automation.conditions[index].values = '';
|
this.automation.conditions[index].values = '';
|
||||||
},
|
},
|
||||||
|
resetAction(index) {
|
||||||
|
this.automation.actions[index].action_params = [];
|
||||||
|
},
|
||||||
showUserInput(operatorType) {
|
showUserInput(operatorType) {
|
||||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
showActionInput(actionName) {
|
||||||
|
if (actionName === 'send_email_to_team' || actionName === 'send_message')
|
||||||
|
return false;
|
||||||
|
const type = AUTOMATION_ACTION_TYPES.find(
|
||||||
|
action => action.key === actionName
|
||||||
|
).inputType;
|
||||||
|
if (type === null) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -97,6 +97,9 @@
|
||||||
:dropdown-values="
|
:dropdown-values="
|
||||||
getActionDropdownValues(automation.actions[i].action_name)
|
getActionDropdownValues(automation.actions[i].action_name)
|
||||||
"
|
"
|
||||||
|
:show-action-input="
|
||||||
|
showActionInput(automation.actions[i].action_name)
|
||||||
|
"
|
||||||
:v="$v.automation.actions.$each[i]"
|
:v="$v.automation.actions.$each[i]"
|
||||||
@removeAction="removeAction(i)"
|
@removeAction="removeAction(i)"
|
||||||
/>
|
/>
|
||||||
|
@ -192,7 +195,13 @@ export default {
|
||||||
required,
|
required,
|
||||||
$each: {
|
$each: {
|
||||||
action_params: {
|
action_params: {
|
||||||
required,
|
required: requiredIf(prop => {
|
||||||
|
return !(
|
||||||
|
prop.action_name === 'mute_conversation' ||
|
||||||
|
prop.action_name === 'snooze_conversation' ||
|
||||||
|
prop.action_name === 'resolve_conversation'
|
||||||
|
);
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -246,7 +255,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.formatConditions(this.selectedResponse);
|
this.formatAutomation(this.selectedResponse);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onEventChange() {
|
onEventChange() {
|
||||||
|
@ -415,14 +424,15 @@ export default {
|
||||||
submitAutomation() {
|
submitAutomation() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) return;
|
if (this.$v.$invalid) return;
|
||||||
this.automation.conditions[
|
const automation = JSON.parse(JSON.stringify(this.automation));
|
||||||
this.automation.conditions.length - 1
|
automation.conditions[
|
||||||
|
automation.conditions.length - 1
|
||||||
].query_operator = null;
|
].query_operator = null;
|
||||||
this.automation.conditions = filterQueryGenerator(
|
automation.conditions = filterQueryGenerator(
|
||||||
this.automation.conditions
|
automation.conditions
|
||||||
).payload;
|
).payload;
|
||||||
this.automation.actions = actionQueryGenerator(this.automation.actions);
|
automation.actions = actionQueryGenerator(automation.actions);
|
||||||
this.$emit('saveAutomation', this.automation, 'EDIT');
|
this.$emit('saveAutomation', automation, 'EDIT');
|
||||||
},
|
},
|
||||||
resetFilter(index, currentCondition) {
|
resetFilter(index, currentCondition) {
|
||||||
this.automation.conditions[index].filter_operator = this.automationTypes[
|
this.automation.conditions[index].filter_operator = this.automationTypes[
|
||||||
|
@ -437,7 +447,7 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
formatConditions(automation) {
|
formatAutomation(automation) {
|
||||||
const formattedConditions = automation.conditions.map(condition => {
|
const formattedConditions = automation.conditions.map(condition => {
|
||||||
const inputType = this.automationTypes[
|
const inputType = this.automationTypes[
|
||||||
automation.event_name
|
automation.event_name
|
||||||
|
@ -457,11 +467,29 @@ export default {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const formattedActions = automation.actions.map(action => {
|
const formattedActions = automation.actions.map(action => {
|
||||||
|
let actionParams = [];
|
||||||
|
if (action.action_params.length) {
|
||||||
|
const inputType = AUTOMATION_ACTION_TYPES.find(
|
||||||
|
item => item.key === action.action_name
|
||||||
|
).inputType;
|
||||||
|
if (inputType === 'multi_select') {
|
||||||
|
actionParams = [
|
||||||
|
...this.getActionDropdownValues(action.action_name),
|
||||||
|
].filter(item => [...action.action_params].includes(item.id));
|
||||||
|
} else if (inputType === 'team_message') {
|
||||||
|
actionParams = {
|
||||||
|
team_ids: [
|
||||||
|
...this.getActionDropdownValues(action.action_name),
|
||||||
|
].filter(item =>
|
||||||
|
[...action.action_params[0].team_ids].includes(item.id)
|
||||||
|
),
|
||||||
|
message: action.action_params[0].message,
|
||||||
|
};
|
||||||
|
} else actionParams = [...action.action_params];
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...action,
|
...action,
|
||||||
action_params: [
|
action_params: actionParams,
|
||||||
...this.getActionDropdownValues(action.action_name),
|
|
||||||
].filter(item => [...action.action_params].includes(item.id)),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.automation = {
|
this.automation = {
|
||||||
|
@ -470,6 +498,15 @@ export default {
|
||||||
actions: formattedActions,
|
actions: formattedActions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
showActionInput(actionName) {
|
||||||
|
if (actionName === 'send_email_to_team' || actionName === 'send_message')
|
||||||
|
return false;
|
||||||
|
const type = AUTOMATION_ACTION_TYPES.find(
|
||||||
|
action => action.key === actionName
|
||||||
|
).inputType;
|
||||||
|
if (type === null) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -34,19 +34,10 @@
|
||||||
<td>{{ automation.name }}</td>
|
<td>{{ automation.name }}</td>
|
||||||
<td>{{ automation.description }}</td>
|
<td>{{ automation.description }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<woot-switch
|
||||||
type="button"
|
:value="automation.active"
|
||||||
class="toggle-button"
|
@input="toggleAutomation(automation, automation.active)"
|
||||||
:class="{ active: automation.active }"
|
/>
|
||||||
role="switch"
|
|
||||||
:aria-checked="automation.active.toString()"
|
|
||||||
@click="toggleAutomation(automation, automation.active)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
:class="{ active: automation.active }"
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
<td>{{ readableTime(automation.created_on) }}</td>
|
<td>{{ readableTime(automation.created_on) }}</td>
|
||||||
<td class="button-wrapper">
|
<td class="button-wrapper">
|
||||||
|
@ -238,7 +229,6 @@ export default {
|
||||||
mode === 'EDIT'
|
mode === 'EDIT'
|
||||||
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
|
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
|
||||||
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
|
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
|
||||||
|
|
||||||
await await this.$store.dispatch(action, payload);
|
await await this.$store.dispatch(action, payload);
|
||||||
this.showAlert(this.$t(successMessage));
|
this.showAlert(this.$t(successMessage));
|
||||||
this.hideAddPopup();
|
this.hideAddPopup();
|
||||||
|
@ -263,7 +253,7 @@ export default {
|
||||||
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION', {
|
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION', {
|
||||||
automationName: automation.name,
|
automationName: automation.name,
|
||||||
});
|
});
|
||||||
// Check if uses confirms to proceed
|
// Check if user confirms to proceed
|
||||||
const ok = await this.$refs.confirmDialog.showConfirmation();
|
const ok = await this.$refs.confirmDialog.showConfirmation();
|
||||||
if (ok) {
|
if (ok) {
|
||||||
await await this.$store.dispatch('automations/update', {
|
await await this.$store.dispatch('automations/update', {
|
||||||
|
@ -290,41 +280,4 @@ export default {
|
||||||
.automation__status-checkbox {
|
.automation__status-checkbox {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.toggle-button {
|
|
||||||
background-color: var(--s-200);
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
height: 19px;
|
|
||||||
width: 34px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-radius: var(--border-radius-large);
|
|
||||||
cursor: pointer;
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 200ms;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-button.active {
|
|
||||||
background-color: var(--w-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-button span {
|
|
||||||
--space-one-point-five: 1.5rem;
|
|
||||||
height: var(--space-one-point-five);
|
|
||||||
width: var(--space-one-point-five);
|
|
||||||
display: inline-block;
|
|
||||||
background-color: var(--white);
|
|
||||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
|
||||||
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
|
||||||
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
|
||||||
transform: translate(0, 0);
|
|
||||||
border-radius: 100%;
|
|
||||||
transition-property: transform;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 200ms;
|
|
||||||
}
|
|
||||||
.toggle-button span.active {
|
|
||||||
transform: translate(var(--space-one-point-five), var(--space-zero));
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -76,10 +76,46 @@ export const AUTOMATIONS = {
|
||||||
name: 'Add a label',
|
name: 'Add a label',
|
||||||
attributeI18nKey: 'ADD_LABEL',
|
attributeI18nKey: 'ADD_LABEL',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_to_team',
|
||||||
|
name: 'Send an email to team',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_message',
|
||||||
|
name: 'Send a message',
|
||||||
|
attributeI18nKey: 'SEND_MESSAGE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
name: 'Send an email transcript',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
name: 'Mute conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_conversation',
|
||||||
|
name: 'Snooze conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: 'resolve_conversation',
|
||||||
|
name: 'Resolve conversation',
|
||||||
|
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
name: 'Send Webhook Event',
|
||||||
|
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'send_email_to_team',
|
// key: 'send_attachment',
|
||||||
// name: 'Send an email to team',
|
// name: 'Send Attachment',
|
||||||
// attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
|
// attributeI18nKey: 'SEND_ATTACHMENT',
|
||||||
// },
|
// },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -107,7 +143,7 @@ export const AUTOMATIONS = {
|
||||||
filterOperators: OPERATOR_TYPES_1,
|
filterOperators: OPERATOR_TYPES_1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'referrer',
|
key: 'referer',
|
||||||
name: 'Referrer Link',
|
name: 'Referrer Link',
|
||||||
attributeI18nKey: 'REFERER_LINK',
|
attributeI18nKey: 'REFERER_LINK',
|
||||||
inputType: 'plain_text',
|
inputType: 'plain_text',
|
||||||
|
@ -120,16 +156,51 @@ export const AUTOMATIONS = {
|
||||||
name: 'Assign a team',
|
name: 'Assign a team',
|
||||||
attributeI18nKey: 'ASSIGN_TEAM',
|
attributeI18nKey: 'ASSIGN_TEAM',
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// key: 'send_email_to_team',
|
|
||||||
// name: 'Send an email to team',
|
|
||||||
// attributeI18nKey: 'SEND_MESSAGE',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: 'assign_agent',
|
key: 'assign_agent',
|
||||||
name: 'Assign an agent',
|
name: 'Assign an agent',
|
||||||
attributeI18nKey: 'ASSIGN_AGENT',
|
attributeI18nKey: 'ASSIGN_AGENT',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_to_team',
|
||||||
|
name: 'Send an email to team',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_message',
|
||||||
|
name: 'Send a message',
|
||||||
|
attributeI18nKey: 'SEND_MESSAGE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
name: 'Send an email transcript',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
name: 'Mute conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_conversation',
|
||||||
|
name: 'Snooze conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_conversation',
|
||||||
|
name: 'Resolve conversation',
|
||||||
|
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
name: 'Send Webhook Event',
|
||||||
|
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// key: 'send_attachment',
|
||||||
|
// name: 'Send Attachment',
|
||||||
|
// attributeI18nKey: 'SEND_ATTACHMENT',
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
conversation_updated: {
|
conversation_updated: {
|
||||||
|
@ -183,17 +254,51 @@ export const AUTOMATIONS = {
|
||||||
name: 'Assign a team',
|
name: 'Assign a team',
|
||||||
attributeI18nKey: 'ASSIGN_TEAM',
|
attributeI18nKey: 'ASSIGN_TEAM',
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// key: 'send_email_to_team',
|
|
||||||
// name: 'Send an email to team',
|
|
||||||
// attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: 'assign_agent',
|
key: 'assign_agent',
|
||||||
name: 'Assign an agent',
|
name: 'Assign an agent',
|
||||||
attributeI18nKey: 'ASSIGN_AGENT',
|
attributeI18nKey: 'ASSIGN_AGENT',
|
||||||
attributeKey: 'assignee_id',
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_to_team',
|
||||||
|
name: 'Send an email to team',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_message',
|
||||||
|
name: 'Send a message',
|
||||||
|
attributeI18nKey: 'SEND_MESSAGE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
name: 'Send an email transcript',
|
||||||
|
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
name: 'Mute conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_conversation',
|
||||||
|
name: 'Snooze conversation',
|
||||||
|
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_conversation',
|
||||||
|
name: 'Resolve conversation',
|
||||||
|
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
name: 'Send Webhook Event',
|
||||||
|
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// key: 'send_attachment',
|
||||||
|
// name: 'Send Attachment',
|
||||||
|
// attributeI18nKey: 'SEND_ATTACHMENT',
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -217,13 +322,51 @@ export const AUTOMATION_ACTION_TYPES = [
|
||||||
{
|
{
|
||||||
key: 'assign_team',
|
key: 'assign_team',
|
||||||
label: 'Assign a team',
|
label: 'Assign a team',
|
||||||
|
inputType: 'multi_select',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'add_label',
|
key: 'add_label',
|
||||||
label: 'Add a label',
|
label: 'Add a label',
|
||||||
|
inputType: 'multi_select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_to_team',
|
||||||
|
label: 'Send an email to team',
|
||||||
|
inputType: 'team_message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_email_transcript',
|
||||||
|
label: 'Send an email transcript',
|
||||||
|
inputType: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mute_conversation',
|
||||||
|
label: 'Mute conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'snooze_conversation',
|
||||||
|
label: 'Snooze conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'resolve_conversation',
|
||||||
|
label: 'Resolve conversation',
|
||||||
|
inputType: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'send_webhook_event',
|
||||||
|
label: 'Send Webhook Event',
|
||||||
|
inputType: 'url',
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'send_email_to_team',
|
// key: 'send_attachment',
|
||||||
// label: 'Send an email to team',
|
// label: 'Send Attachment',
|
||||||
|
// inputType: 'file',
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
key: 'send_message',
|
||||||
|
label: 'Send a message',
|
||||||
|
inputType: 'textarea',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue