Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
ccb8ced4e5
290 changed files with 5676 additions and 1809 deletions
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 }}
|
3
Gemfile
3
Gemfile
|
@ -125,6 +125,9 @@ gem 'procore-sift'
|
|||
gem 'email_reply_trimmer'
|
||||
gem 'html2text'
|
||||
|
||||
# to calculate working hours
|
||||
gem 'working_hours'
|
||||
|
||||
group :production, :staging do
|
||||
# we dont want request timing out in development while using byebug
|
||||
gem 'rack-timeout'
|
||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -378,14 +378,14 @@ GEM
|
|||
netrc (0.11.0)
|
||||
newrelic_rpm (8.4.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.3)
|
||||
nokogiri (1.13.4)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.3-arm64-darwin)
|
||||
nokogiri (1.13.4-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.3-x86_64-darwin)
|
||||
nokogiri (1.13.4-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.13.3-x86_64-linux)
|
||||
nokogiri (1.13.4-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.8)
|
||||
orm_adapter (0.5.0)
|
||||
|
@ -636,6 +636,9 @@ GEM
|
|||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
|
@ -746,6 +749,7 @@ DEPENDENCIES
|
|||
webpacker (~> 5.x)
|
||||
webpush
|
||||
wisper (= 2.0.0)
|
||||
working_hours
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.0.2p107
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Campaigns::CampaignConversationBuilder
|
||||
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes]
|
||||
pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes, :custom_attributes]
|
||||
|
||||
def perform
|
||||
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
||||
|
@ -32,7 +32,8 @@ class Campaigns::CampaignConversationBuilder
|
|||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
campaign_id: @campaign.id,
|
||||
additional_attributes: conversation_additional_attributes
|
||||
additional_attributes: conversation_additional_attributes,
|
||||
custom_attributes: custom_attributes || {}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ class Messages::MessageBuilder
|
|||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
|
@ -64,6 +65,10 @@ class Messages::MessageBuilder
|
|||
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
||||
end
|
||||
|
||||
def automation_rule_id
|
||||
@automation_rule.present? ? { content_attributes: { automation_rule_id: @automation_rule } } : {}
|
||||
end
|
||||
|
||||
def message_sender
|
||||
return if @params[:sender_type] != 'AgentBot'
|
||||
|
||||
|
@ -82,6 +87,6 @@ class Messages::MessageBuilder
|
|||
items: @items,
|
||||
in_reply_to: @in_reply_to,
|
||||
echo_id: @params[:echo_id]
|
||||
}.merge(external_created_at)
|
||||
}.merge(external_created_at).merge(automation_rule_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ class V2::ReportBuilder
|
|||
attr_reader :account, :params
|
||||
|
||||
DEFAULT_GROUP_BY = 'day'.freeze
|
||||
AGENT_RESULTS_PER_PAGE = 10
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
|
@ -79,12 +80,15 @@ class V2::ReportBuilder
|
|||
end
|
||||
|
||||
def agent_metrics
|
||||
users = @account.users
|
||||
users = users.where(id: params[:user_id]) if params[:user_id].present?
|
||||
users.each_with_object([]) do |user, arr|
|
||||
@user = user
|
||||
account_users = @account.account_users.page(params[:page]).per(AGENT_RESULTS_PER_PAGE)
|
||||
account_users.each_with_object([]) do |account_user, arr|
|
||||
@user = account_user.user
|
||||
arr << {
|
||||
user: { id: user.id, name: user.name, thumbnail: user.avatar_url },
|
||||
id: @user.id,
|
||||
name: @user.name,
|
||||
email: @user.email,
|
||||
thumbnail: @user.avatar_url,
|
||||
availability: account_user.availability_status,
|
||||
metric: conversations
|
||||
}
|
||||
end
|
||||
|
@ -94,7 +98,7 @@ class V2::ReportBuilder
|
|||
@open_conversations = scope.conversations.open
|
||||
first_response_count = scope.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||
metric = {
|
||||
open: @open_conversations.count,
|
||||
total: @open_conversations.count,
|
||||
unattended: @open_conversations.count - first_response_count
|
||||
}
|
||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||
|
|
|
@ -17,12 +17,29 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||
@automation_rule
|
||||
end
|
||||
|
||||
def attach_file
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@automation_rule.update(automation_rules_permit)
|
||||
process_attachments
|
||||
@automation_rule
|
||||
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
|
||||
|
||||
def destroy
|
||||
|
@ -37,16 +54,19 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||
@automation_rule = new_rule
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_attachments
|
||||
return if params[:attachments].blank?
|
||||
actions = @automation_rule.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||
return if actions.blank?
|
||||
|
||||
params[:attachments].each do |uploaded_attachment|
|
||||
@automation_rule.files.attach(uploaded_attachment)
|
||||
actions.each do |action|
|
||||
blob_id = action['action_params']
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
@automation_rule.files.attach(blob)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def automation_rules_permit
|
||||
params.permit(
|
||||
:name, :description, :event_name, :account_id, :active,
|
||||
|
|
|
@ -4,6 +4,6 @@ class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseControll
|
|||
private
|
||||
|
||||
def portal
|
||||
@portal ||= Current.account.kbase_portals.find_by(id: params[:portal_id])
|
||||
@portal ||= Current.account.kbase_portals.find_by(slug: params[:portal_id])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::BaseController
|
||||
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_portal, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@portals = Current.account.kbase_portals
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@portal = Current.account.kbase_portals.create!(portal_params)
|
||||
end
|
||||
|
@ -21,7 +23,11 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba
|
|||
private
|
||||
|
||||
def fetch_portal
|
||||
@portal = current_account.kbase_portals.find(params[:id])
|
||||
@portal = Current.account.kbase_portals.find_by(slug: permitted_params[:id])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id)
|
||||
end
|
||||
|
||||
def portal_params
|
||||
|
|
|
@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
|||
private
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:inbox_id, :url)
|
||||
params.require(:webhook).permit(:inbox_id, :url, subscriptions: [])
|
||||
end
|
||||
|
||||
def fetch_webhook
|
||||
|
|
|
@ -39,7 +39,8 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
browser: browser_params,
|
||||
referer: permitted_params[:message][:referer_url],
|
||||
initiated_at: timestamp_params
|
||||
}
|
||||
},
|
||||
custom_attributes: permitted_params[:custom_attributes].presence || {}
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -52,16 +53,33 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
mergee_contact: @contact
|
||||
).perform
|
||||
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
|
||||
|
||||
def contact_email
|
||||
permitted_params[:contact][:email].downcase
|
||||
permitted_params.dig(:contact, :email)&.downcase
|
||||
end
|
||||
|
||||
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
|
||||
permitted_params.dig(:contact, :phone_number)
|
||||
end
|
||||
|
||||
def browser_params
|
||||
|
|
|
@ -7,12 +7,18 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||
process_update_contact
|
||||
@conversation = create_conversation
|
||||
conversation.messages.create(message_params)
|
||||
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
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
|
@ -63,6 +69,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
end
|
||||
|
||||
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, :phone_number],
|
||||
message: [:content, :referer_url, :timestamp, :echo_id],
|
||||
custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,42 +47,43 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def current_summary_params
|
||||
def common_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
id: params[:id],
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until],
|
||||
group_by: params[:group_by]
|
||||
group_by: params[:group_by],
|
||||
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||
}
|
||||
end
|
||||
|
||||
def current_summary_params
|
||||
common_params.merge({
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until]
|
||||
})
|
||||
end
|
||||
|
||||
def previous_summary_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
id: params[:id],
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until],
|
||||
group_by: params[:group_by]
|
||||
}
|
||||
common_params.merge({
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until]
|
||||
})
|
||||
end
|
||||
|
||||
def report_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: params[:type].to_sym,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id],
|
||||
group_by: params[:group_by],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
}
|
||||
common_params.merge({
|
||||
metric: params[:metric],
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
})
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
type: params[:type].to_sym,
|
||||
user_id: params[:user_id]
|
||||
user_id: params[:user_id],
|
||||
page: params[:page].presence || 1
|
||||
}
|
||||
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
|
||||
retriever_method :imap, { address: channel_data[:imap_address],
|
||||
port: channel_data[:imap_port],
|
||||
user_name: channel_data[:imap_email],
|
||||
user_name: channel_data[:imap_login],
|
||||
password: channel_data[:imap_password],
|
||||
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||
end
|
||||
|
@ -29,8 +29,12 @@ module Api::V1::InboxesHelper
|
|||
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
|
|
|
@ -33,17 +33,23 @@ module ReportHelper
|
|||
end
|
||||
|
||||
def avg_first_response_time
|
||||
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
|
||||
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
|
||||
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
|
||||
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
|
||||
avg_rt = scope.reporting_events
|
||||
.where(name: 'conversation_resolved', created_at: range)
|
||||
.average(:value)
|
||||
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?
|
||||
|
||||
|
@ -51,9 +57,9 @@ module ReportHelper
|
|||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
avg_frt = scope.reporting_events
|
||||
.where(name: 'first_response', created_at: range)
|
||||
.average(:value)
|
||||
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?
|
||||
|
||||
|
|
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>
|
||||
<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" />
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view></router-view>
|
||||
|
@ -11,21 +11,28 @@
|
|||
<woot-snackbar-box />
|
||||
<network-notification />
|
||||
</div>
|
||||
<loading-state v-else />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
} from './helper/pushHelper';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
AddAccountModal,
|
||||
LoadingState,
|
||||
NetworkNotification,
|
||||
UpdateBanner,
|
||||
WootSnackbarBox,
|
||||
|
@ -43,13 +50,12 @@ export default {
|
|||
getAccount: 'accounts/getAccount',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
authUIFlags: 'getAuthUIFlags',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
hasAccounts() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.accounts &&
|
||||
this.currentUser.accounts.length !== 0
|
||||
);
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
return accounts.length > 0;
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -58,32 +64,37 @@ export default {
|
|||
if (!this.hasAccounts) {
|
||||
this.showAddAccountModal = true;
|
||||
}
|
||||
verifyServiceWorkerExistence(registration =>
|
||||
registration.pushManager.getSubscription().then(subscription => {
|
||||
if (subscription) {
|
||||
registerSubscription();
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
currentAccountId() {
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setUser');
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
this.initializeAccount();
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
},
|
||||
|
||||
async initializeAccount() {
|
||||
const { pathname } = window.location;
|
||||
const accountId = accountIdFromPathname(pathname);
|
||||
|
||||
if (accountId) {
|
||||
await this.$store.dispatch('accounts/get');
|
||||
const {
|
||||
locale,
|
||||
latest_chatwoot_version: latestChatwootVersion,
|
||||
} = this.getAccount(accountId);
|
||||
this.setLocale(locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
}
|
||||
await this.$store.dispatch('accounts/get');
|
||||
const {
|
||||
locale,
|
||||
latest_chatwoot_version: latestChatwootVersion,
|
||||
} = this.getAccount(this.currentAccountId);
|
||||
const { pubsub_token: pubsubToken } = this.currentUser || {};
|
||||
this.setLocale(locale);
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
vueActionCable.init(pubsubToken);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
/* eslint no-console: 0 */
|
||||
/* global axios */
|
||||
/* eslint no-undef: "error" */
|
||||
|
||||
import Cookies from 'js-cookie';
|
||||
import endPoints from './endPoints';
|
||||
|
@ -61,41 +59,15 @@ export default {
|
|||
});
|
||||
return fetchPromise;
|
||||
},
|
||||
|
||||
isLoggedIn() {
|
||||
const hasAuthCookie = !!Cookies.getJSON('auth_data');
|
||||
const hasUserCookie = !!Cookies.getJSON('user');
|
||||
return hasAuthCookie && hasUserCookie;
|
||||
hasAuthCookie() {
|
||||
return !!Cookies.getJSON('cw_d_session_info');
|
||||
},
|
||||
|
||||
isAdmin() {
|
||||
if (this.isLoggedIn()) {
|
||||
return Cookies.getJSON('user').role === 'administrator';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getAuthData() {
|
||||
if (this.isLoggedIn()) {
|
||||
return Cookies.getJSON('auth_data');
|
||||
if (this.hasAuthCookie()) {
|
||||
return Cookies.getJSON('cw_d_session_info');
|
||||
}
|
||||
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 }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
|
|
|
@ -9,6 +9,14 @@ class AutomationsAPI extends ApiClient {
|
|||
clone(automationId) {
|
||||
return axios.post(`${this.url}/${automationId}/clone`);
|
||||
}
|
||||
|
||||
attachment(file) {
|
||||
return axios.post(`${this.url}/attach_file`, file, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutomationsAPI();
|
||||
|
|
|
@ -8,7 +8,15 @@ class ReportsAPI extends ApiClient {
|
|||
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}`, {
|
||||
params: {
|
||||
metric,
|
||||
|
@ -17,12 +25,13 @@ class ReportsAPI extends ApiClient {
|
|||
type,
|
||||
id,
|
||||
group_by,
|
||||
business_hours,
|
||||
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`, {
|
||||
params: {
|
||||
since,
|
||||
|
@ -30,6 +39,16 @@ class ReportsAPI extends ApiClient {
|
|||
type,
|
||||
id,
|
||||
group_by,
|
||||
business_hours,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getConversationMetric(type = 'account', page = 1) {
|
||||
return axios.get(`${this.url}/conversations`, {
|
||||
params: {
|
||||
type,
|
||||
page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -97,5 +97,18 @@ describe('#Reports API', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getConversationMetric', () => {
|
||||
reportsAPI.getConversationMetric('account');
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/conversations',
|
||||
{
|
||||
params: {
|
||||
type: 'account',
|
||||
page: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.margin-bottom-small {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.margin-right-smaller {
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
|
@ -51,10 +55,6 @@
|
|||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.text-y-800 {
|
||||
color: var(--y-800);
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -78,5 +78,10 @@
|
|||
font-size: $font-size-default;
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
.business-hours {
|
||||
margin: $space-normal;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,3 +25,21 @@
|
|||
align-items: center;
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
<p class="sub-head">
|
||||
{{ subTitle }}
|
||||
</p>
|
||||
<p v-if="note">
|
||||
<span class="note">{{ $t('INBOX_MGMT.NOTE') }}</span>
|
||||
{{ note }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="medium-6 small-12">
|
||||
<slot></slot>
|
||||
|
@ -25,6 +29,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
note: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -46,5 +54,9 @@ export default {
|
|||
.title--section {
|
||||
padding-right: var(--space-large);
|
||||
}
|
||||
|
||||
.note {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -53,7 +53,7 @@ export default {
|
|||
computed: {
|
||||
...mapGetters({
|
||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||
getCurrentAccountId: 'getCurrentAccountId',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
availabilityDisplayLabel() {
|
||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||
|
@ -63,9 +63,6 @@ export default {
|
|||
availabilityIndex
|
||||
];
|
||||
},
|
||||
currentAccountId() {
|
||||
return this.getCurrentAccountId;
|
||||
},
|
||||
currentUserAvailability() {
|
||||
return this.getCurrentUserAvailability;
|
||||
},
|
||||
|
|
|
@ -3,7 +3,8 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
|||
const reports = accountId => ({
|
||||
parentNav: 'reports',
|
||||
routes: [
|
||||
'settings_account_reports',
|
||||
'account_overview_reports',
|
||||
'conversation_reports',
|
||||
'csat_reports',
|
||||
'agent_reports',
|
||||
'label_reports',
|
||||
|
@ -16,7 +17,14 @@ const reports = accountId => ({
|
|||
label: 'REPORTS_OVERVIEW',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||
toStateName: 'settings_account_reports',
|
||||
toStateName: 'account_overview_reports',
|
||||
},
|
||||
{
|
||||
icon: 'chat',
|
||||
label: 'REPORTS_CONVERSATION',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/conversation`),
|
||||
toStateName: 'conversation_reports',
|
||||
},
|
||||
{
|
||||
icon: 'emoji',
|
||||
|
|
|
@ -85,14 +85,20 @@ export default {
|
|||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
||||
toStateName: 'settings_inbox_new',
|
||||
newLinkRouteName: 'settings_inbox_new',
|
||||
children: this.inboxes.map(inbox => ({
|
||||
id: inbox.id,
|
||||
label: inbox.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(`accounts/${this.accountId}/inbox/${inbox.id}`),
|
||||
type: inbox.channel_type,
|
||||
phoneNumber: inbox.phone_number,
|
||||
})),
|
||||
children: this.inboxes
|
||||
.map(inbox => ({
|
||||
id: inbox.id,
|
||||
label: inbox.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`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() {
|
||||
|
|
|
@ -112,10 +112,10 @@ export default {
|
|||
}
|
||||
|
||||
&.warning {
|
||||
background: var(--y-800);
|
||||
color: var(--s-600);
|
||||
background: var(--y-600);
|
||||
color: var(--y-500);
|
||||
a {
|
||||
color: var(--s-600);
|
||||
color: var(--y-500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ export default {
|
|||
&.warning {
|
||||
background: var(--y-100);
|
||||
color: var(--y-900);
|
||||
border: 1px solid var(--y-300);
|
||||
border: 1px solid var(--y-200);
|
||||
a {
|
||||
color: var(--y-900);
|
||||
}
|
||||
|
|
|
@ -1,54 +1,70 @@
|
|||
<template>
|
||||
<label class="switch" :class="classObject">
|
||||
<input
|
||||
:id="id"
|
||||
v-model="value"
|
||||
class="switch-input"
|
||||
:name="name"
|
||||
:disabled="disabled"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div class="switch-paddle" :for="name">
|
||||
<span class="show-for-sr">on off</span>
|
||||
</div>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
:class="{ active: value }"
|
||||
role="switch"
|
||||
:aria-checked="value.toString()"
|
||||
@click="onClick"
|
||||
>
|
||||
<span aria-hidden="true" :class="{ active: value }"></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
type: { type: String, default: '' },
|
||||
size: { type: String, default: '' },
|
||||
checked: Boolean,
|
||||
name: { type: String, default: '' },
|
||||
id: { type: String, default: '' },
|
||||
value: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classObject() {
|
||||
const { type, size, value } = this;
|
||||
return {
|
||||
[`is-${type}`]: type,
|
||||
[`${size}`]: size,
|
||||
checked: value,
|
||||
};
|
||||
methods: {
|
||||
onClick(event) {
|
||||
if (event.pointerId === -1) {
|
||||
event.preventDefault();
|
||||
} else {
|
||||
this.$emit('input', !this.value);
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.value = this.checked;
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('input', (this.value = !!this.checked));
|
||||
},
|
||||
};
|
||||
</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,7 @@
|
|||
<select
|
||||
v-model="action_name"
|
||||
class="action__question"
|
||||
:class="{ 'full-width': !inputType }"
|
||||
:class="{ 'full-width': !showActionInput }"
|
||||
@change="resetAction()"
|
||||
>
|
||||
<option
|
||||
|
@ -52,6 +52,11 @@
|
|||
class="answer--text-input"
|
||||
placeholder="Enter url"
|
||||
/>
|
||||
<automation-action-file-input
|
||||
v-if="inputType === 'attachment'"
|
||||
v-model="action_params"
|
||||
:initial-file-name="initialFileName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
|
@ -61,6 +66,18 @@
|
|||
@click="removeAction"
|
||||
/>
|
||||
</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
|
||||
v-if="v.action_params.$dirty && v.action_params.$error"
|
||||
class="filter-error"
|
||||
|
@ -71,7 +88,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
|
||||
import AutomationActionFileInput from './AutomationFileInput.vue';
|
||||
export default {
|
||||
components: {
|
||||
AutomationActionTeamMessageInput,
|
||||
AutomationActionFileInput,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
|
@ -93,6 +116,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
initialFileName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
action_name: {
|
||||
|
@ -171,6 +198,7 @@ export default {
|
|||
.filter__answer--wrap {
|
||||
margin-right: var(--space-smaller);
|
||||
flex-grow: 1;
|
||||
max-width: 50%;
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
|
@ -211,4 +239,7 @@ export default {
|
|||
.multiselect {
|
||||
margin-bottom: var(--space-zero);
|
||||
}
|
||||
.action-message {
|
||||
margin: var(--space-small) 0 0;
|
||||
}
|
||||
</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>
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<label class="input-wrapper" :class="uploadState">
|
||||
<input
|
||||
v-if="uploadState !== 'processing'"
|
||||
type="file"
|
||||
name="attachment"
|
||||
:class="uploadState === 'processing' ? 'disabled' : ''"
|
||||
@change="onChangeFile"
|
||||
/>
|
||||
<spinner v-if="uploadState === 'processing'" />
|
||||
<fluent-icon v-if="uploadState === 'idle'" icon="file-upload" />
|
||||
<fluent-icon
|
||||
v-if="uploadState === 'uploaded'"
|
||||
icon="checkmark-circle"
|
||||
type="outline"
|
||||
class="success-icon"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-if="uploadState === 'failed'"
|
||||
icon="dismiss-circle"
|
||||
type="outline"
|
||||
class="error-icon"
|
||||
/>
|
||||
<p class="file-button">{{ label }}</p>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
initialFileName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploadState: 'idle',
|
||||
label: this.$t('AUTOMATION.ATTACHMENT.LABEL_IDLE'),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.initialFileName) {
|
||||
this.label = this.initialFileName;
|
||||
this.uploadState = 'uploaded';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onChangeFile(event) {
|
||||
this.uploadState = 'processing';
|
||||
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
|
||||
try {
|
||||
const file = event.target.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', file, file.name);
|
||||
const id = await this.$store.dispatch(
|
||||
'automations/uploadAttachment',
|
||||
formData
|
||||
);
|
||||
this.$emit('input', [id]);
|
||||
this.uploadState = 'uploaded';
|
||||
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADED');
|
||||
} catch (error) {
|
||||
this.uploadState = 'failed';
|
||||
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
|
||||
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
height: 39px;
|
||||
background-color: var(--white);
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 1px dashed var(--w-100);
|
||||
padding: var(--space-smaller) var(--space-small);
|
||||
align-items: center;
|
||||
font-size: var(--font-size-mini);
|
||||
cursor: pointer;
|
||||
}
|
||||
.success-icon {
|
||||
margin-right: var(--space-small);
|
||||
color: var(--g-500);
|
||||
}
|
||||
.error-icon {
|
||||
margin-right: var(--space-small);
|
||||
color: var(--r-500);
|
||||
}
|
||||
|
||||
.processing {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.file-button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
50
app/javascript/dashboard/components/widgets/ShowMore.vue
Normal file
50
app/javascript/dashboard/components/widgets/ShowMore.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<span>
|
||||
{{ textToBeDisplayed }}
|
||||
<button class="show-more--button" @click="toggleShowMore">
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMore: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textToBeDisplayed() {
|
||||
if (this.showMore) {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
return this.text.slice(0, this.limit) + '...';
|
||||
},
|
||||
buttonLabel() {
|
||||
const i18nKey = !this.showMore ? 'SHOW_MORE' : 'SHOW_LESS';
|
||||
return this.$t(`COMPONENTS.SHOW_MORE_BLOCK.${i18nKey}`);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleShowMore() {
|
||||
this.showMore = !this.showMore;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.show-more--button {
|
||||
color: var(--w-500);
|
||||
}
|
||||
</style>
|
|
@ -209,7 +209,7 @@ export default {
|
|||
}
|
||||
|
||||
.user-online-status--busy {
|
||||
background: var(--y-700);
|
||||
background: var(--y-500);
|
||||
}
|
||||
|
||||
.user-online-status--offline {
|
||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
|||
background: var(--s-500);
|
||||
}
|
||||
&__busy {
|
||||
background: var(--y-400);
|
||||
background: var(--y-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||
class="text-y-800"
|
||||
size="14"
|
||||
class="hmac-warning__icon"
|
||||
icon="warning"
|
||||
/>
|
||||
</h3>
|
||||
|
@ -181,7 +181,11 @@ export default {
|
|||
|
||||
.snoozed--display-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--y-900);
|
||||
color: var(--y-600);
|
||||
}
|
||||
}
|
||||
|
||||
.hmac-warning__icon {
|
||||
color: var(--y-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
:readable-time="readableTime"
|
||||
:source-id="data.source_id"
|
||||
:inbox-id="data.inbox_id"
|
||||
:message-read="showReadTicks"
|
||||
/>
|
||||
</div>
|
||||
<spinner v-if="isPending" size="tiny" />
|
||||
|
@ -153,6 +154,14 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasUserReadMessage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isWebWidgetInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -268,6 +277,14 @@ export default {
|
|||
isOutgoing() {
|
||||
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
|
||||
},
|
||||
showReadTicks() {
|
||||
return (
|
||||
(this.isOutgoing || this.isTemplate) &&
|
||||
this.hasUserReadMessage &&
|
||||
this.isWebWidgetInbox &&
|
||||
!this.data.private
|
||||
);
|
||||
},
|
||||
isTemplate() {
|
||||
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
|
||||
},
|
||||
|
|
|
@ -48,6 +48,10 @@
|
|||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:has-user-read-message="
|
||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
||||
"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
/>
|
||||
<li v-show="getUnreadCount != 0" class="unread--toast">
|
||||
<span class="text-uppercase">
|
||||
|
@ -66,6 +70,10 @@
|
|||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:has-user-read-message="
|
||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
||||
"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
|
@ -83,7 +91,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<reply-box
|
||||
v-on-clickaway="closePopoutReplyBox"
|
||||
:conversation-id="currentChat.id"
|
||||
:is-a-tweet="isATweet"
|
||||
:selected-tweet="selectedTweet"
|
||||
|
@ -109,7 +116,6 @@ import inboxMixin from 'shared/mixins/inboxMixin';
|
|||
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
|
||||
import { isEscape } from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -117,7 +123,7 @@ export default {
|
|||
ReplyBox,
|
||||
Banner,
|
||||
},
|
||||
mixins: [conversationMixin, inboxMixin, eventListenerMixins, clickaway],
|
||||
mixins: [conversationMixin, inboxMixin, eventListenerMixins],
|
||||
props: {
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
|
@ -143,6 +149,7 @@ export default {
|
|||
listLoadingStatus: 'getAllMessagesLoaded',
|
||||
getUnreadCount: 'getUnreadCount',
|
||||
loadingChatList: 'getChatListLoadingStatus',
|
||||
conversationLastSeen: 'getConversationLastSeen',
|
||||
}),
|
||||
inboxId() {
|
||||
return this.currentChat.inbox_id;
|
||||
|
@ -243,6 +250,11 @@ export default {
|
|||
}
|
||||
return 'arrow-chevron-left';
|
||||
},
|
||||
getLastSeenAt() {
|
||||
if (this.conversationLastSeen) return this.conversationLastSeen;
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
|
@ -80,8 +80,8 @@
|
|||
>
|
||||
<p
|
||||
v-if="isSignatureAvailable"
|
||||
v-dompurify-html="formatMessage(messageSignature)"
|
||||
class="message-signature"
|
||||
v-html="formatMessage(messageSignature)"
|
||||
/>
|
||||
<p v-else class="message-signature">
|
||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
size="16"
|
||||
/>
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="messageRead"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
||||
icon="checkmark-double"
|
||||
class="action--icon read-tick"
|
||||
size="12"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-if="isEmail"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||
|
@ -120,6 +127,10 @@ export default {
|
|||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
messageRead: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inbox() {
|
||||
|
@ -173,6 +184,10 @@ export default {
|
|||
}
|
||||
|
||||
.action--icon {
|
||||
&.read-tick {
|
||||
color: var(--v-100);
|
||||
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
|
||||
}
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
'hide--quoted': !showQuotedContent,
|
||||
}"
|
||||
>
|
||||
<div class="text-content" v-html="message"></div>
|
||||
<div v-dompurify-html="message" class="text-content"></div>
|
||||
<button
|
||||
v-if="displayQuotedButton"
|
||||
class="quoted-text--button"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint no-console: 0 */
|
||||
import Auth from '../api/auth';
|
||||
|
||||
const parseErrorCode = error => Promise.reject(error);
|
||||
|
@ -7,7 +6,7 @@ export default axios => {
|
|||
const { apiHost = '' } = window.chatwootConfig || {};
|
||||
const wootApi = axios.create({ baseURL: `${apiHost}/` });
|
||||
// Add Auth Headers to requests if logged in
|
||||
if (Auth.isLoggedIn()) {
|
||||
if (Auth.hasAuthCookie()) {
|
||||
const {
|
||||
'access-token': accessToken,
|
||||
'token-type': tokenType,
|
||||
|
|
|
@ -14,6 +14,9 @@ export const getLoginRedirectURL = (ssoAccountId, user) => {
|
|||
if (ssoAccount) {
|
||||
return frontendURL(`accounts/${ssoAccountId}/dashboard`);
|
||||
}
|
||||
if (accounts.length) {
|
||||
return frontendURL(`accounts/${accounts[0].id}/dashboard`);
|
||||
}
|
||||
return DEFAULT_REDIRECT_URL;
|
||||
};
|
||||
|
||||
|
@ -41,15 +44,6 @@ export const conversationUrl = ({
|
|||
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 => {
|
||||
/* 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;
|
||||
|
|
|
@ -23,6 +23,8 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
'contact.updated': this.onContactUpdate,
|
||||
'conversation.mentioned': this.onConversationMentioned,
|
||||
'notification.created': this.onNotificationCreated,
|
||||
'first.reply.created': this.onFirstReplyCreated,
|
||||
'conversation.read': this.onConversationRead,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -64,6 +66,11 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
this.fetchConversationStats();
|
||||
};
|
||||
|
||||
onConversationRead = data => {
|
||||
const { contact_last_seen_at: lastSeen } = data;
|
||||
this.app.$store.dispatch('updateConversationRead', lastSeen);
|
||||
};
|
||||
|
||||
onLogout = () => AuthAPI.logout();
|
||||
|
||||
onMessageCreated = data => {
|
||||
|
@ -122,6 +129,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
|
||||
fetchConversationStats = () => {
|
||||
bus.$emit('fetch_conversation_stats');
|
||||
bus.$emit('fetch_overview_reports');
|
||||
};
|
||||
|
||||
onContactDelete = data => {
|
||||
|
@ -139,17 +147,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
onNotificationCreated = data => {
|
||||
this.app.$store.dispatch('notifications/addNotification', data);
|
||||
};
|
||||
|
||||
onFirstReplyCreated = () => {
|
||||
bus.$emit('fetch_overview_reports');
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
init() {
|
||||
if (AuthAPI.isLoggedIn()) {
|
||||
const actionCable = new ActionCableConnector(
|
||||
window.WOOT,
|
||||
AuthAPI.getPubSubToken()
|
||||
);
|
||||
return actionCable;
|
||||
}
|
||||
return null;
|
||||
init(pubsubToken) {
|
||||
return new ActionCableConnector(window.WOOT, pubsubToken);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
const allElementsString = arr => {
|
||||
return arr.every(elem => typeof elem === 'string');
|
||||
};
|
||||
|
||||
const allElementsNumbers = arr => {
|
||||
return arr.every(elem => typeof elem === 'number');
|
||||
};
|
||||
|
||||
const formatArray = params => {
|
||||
if (params.length <= 0) {
|
||||
params = [];
|
||||
} else if (params.every(elem => typeof elem === 'string')) {
|
||||
} else if (allElementsString(params) || allElementsNumbers(params)) {
|
||||
params = [...params];
|
||||
} else {
|
||||
params = params.map(val => val.id);
|
||||
|
|
100
app/javascript/dashboard/helper/preChat.js
Normal file
100
app/javascript/dashboard/helper/preChat.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import i18n from 'widget/i18n/index';
|
||||
const defaultTranslations = Object.fromEntries(
|
||||
Object.entries(i18n).filter(([key]) => key.includes('en'))
|
||||
).en;
|
||||
|
||||
export const standardFieldKeys = {
|
||||
emailAddress: {
|
||||
key: 'EMAIL_ADDRESS',
|
||||
label: 'Email Id',
|
||||
placeholder: 'Please enter your email address',
|
||||
},
|
||||
fullName: {
|
||||
key: 'FULL_NAME',
|
||||
label: 'Full Name',
|
||||
placeholder: 'Please enter your full name',
|
||||
},
|
||||
phoneNumber: {
|
||||
key: 'PHONE_NUMBER',
|
||||
label: 'Phone Number',
|
||||
placeholder: 'Please enter your phone number',
|
||||
},
|
||||
};
|
||||
|
||||
export const getLabel = ({ key, label }) => {
|
||||
return defaultTranslations.PRE_CHAT_FORM.FIELDS[key]
|
||||
? defaultTranslations.PRE_CHAT_FORM.FIELDS[key].LABEL
|
||||
: label;
|
||||
};
|
||||
export const getPlaceHolder = ({ key, placeholder }) => {
|
||||
return defaultTranslations.PRE_CHAT_FORM.FIELDS[key]
|
||||
? defaultTranslations.PRE_CHAT_FORM.FIELDS[key].PLACEHOLDER
|
||||
: placeholder;
|
||||
};
|
||||
|
||||
export const getCustomFields = ({ standardFields, customAttributes }) => {
|
||||
let customFields = [];
|
||||
const { pre_chat_fields: preChatFields } = standardFields;
|
||||
customAttributes.forEach(attribute => {
|
||||
const itemExist = preChatFields.find(
|
||||
item => item.name === attribute.attribute_key
|
||||
);
|
||||
if (!itemExist) {
|
||||
customFields.push({
|
||||
label: attribute.attribute_display_name,
|
||||
placeholder: attribute.attribute_display_name,
|
||||
name: attribute.attribute_key,
|
||||
type: attribute.attribute_display_type,
|
||||
values: attribute.attribute_values,
|
||||
field_type: attribute.attribute_model,
|
||||
required: false,
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
return customFields;
|
||||
};
|
||||
|
||||
export const getFormattedPreChatFields = ({ preChatFields }) => {
|
||||
return preChatFields.map(item => {
|
||||
return {
|
||||
...item,
|
||||
label: getLabel({
|
||||
key: standardFieldKeys[item.name]
|
||||
? standardFieldKeys[item.name].key
|
||||
: item.name,
|
||||
label: item.label ? item.label : item.name,
|
||||
}),
|
||||
placeholder: getPlaceHolder({
|
||||
key: standardFieldKeys[item.name]
|
||||
? standardFieldKeys[item.name].key
|
||||
: item.name,
|
||||
placeholder: item.placeholder ? item.placeholder : item.name,
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getPreChatFields = ({
|
||||
preChatFormOptions = {},
|
||||
customAttributes = [],
|
||||
}) => {
|
||||
const { pre_chat_message, pre_chat_fields } = preChatFormOptions;
|
||||
let customFields = {};
|
||||
let preChatFields = {};
|
||||
|
||||
const formattedPreChatFields = getFormattedPreChatFields({
|
||||
preChatFields: pre_chat_fields,
|
||||
});
|
||||
|
||||
customFields = getCustomFields({
|
||||
standardFields: { pre_chat_fields: formattedPreChatFields },
|
||||
customAttributes,
|
||||
});
|
||||
preChatFields = [...formattedPreChatFields, ...customFields];
|
||||
|
||||
return {
|
||||
pre_chat_message,
|
||||
pre_chat_fields: preChatFields,
|
||||
};
|
||||
};
|
|
@ -44,7 +44,7 @@ export const getPushSubscriptionPayload = subscription => ({
|
|||
});
|
||||
|
||||
export const sendRegistrationToServer = subscription => {
|
||||
if (auth.isLoggedIn()) {
|
||||
if (auth.hasAuthCookie()) {
|
||||
return NotificationSubscriptions.create(
|
||||
getPushSubscriptionPayload(subscription)
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
frontendURL,
|
||||
conversationUrl,
|
||||
accountIdFromPathname,
|
||||
isValidURL,
|
||||
getLoginRedirectURL,
|
||||
} 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', () => {
|
||||
it('should return true if valid url is passed', () => {
|
||||
expect(isValidURL('https://chatwoot.com')).toBe(true);
|
||||
|
@ -75,7 +62,7 @@ describe('#URL Helpers', () => {
|
|||
getLoginRedirectURL('7500', {
|
||||
accounts: [{ id: '7501', name: 'Test Account 7501' }],
|
||||
})
|
||||
).toBe('/app/');
|
||||
).toBe('/app/accounts/7501/dashboard');
|
||||
expect(getLoginRedirectURL('7500', null)).toBe('/app/');
|
||||
});
|
||||
});
|
||||
|
|
47
app/javascript/dashboard/helper/specs/inboxFixture.js
Normal file
47
app/javascript/dashboard/helper/specs/inboxFixture.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
export default {
|
||||
customFields: {
|
||||
pre_chat_message: 'Share your queries or comments here.',
|
||||
pre_chat_fields: [
|
||||
{
|
||||
label: 'Email Address',
|
||||
name: 'emailAddress',
|
||||
type: 'email',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
|
||||
placeholder: 'Please enter your email address',
|
||||
},
|
||||
{
|
||||
label: 'Full Name',
|
||||
name: 'fullName',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
placeholder: 'Please enter your full name',
|
||||
},
|
||||
{
|
||||
label: 'Phone Number',
|
||||
name: 'phoneNumber',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
placeholder: 'Please enter your phone number',
|
||||
},
|
||||
],
|
||||
},
|
||||
customAttributes: [
|
||||
{
|
||||
id: 101,
|
||||
attribute_description: 'Order Identifier',
|
||||
attribute_display_name: 'Order Id',
|
||||
attribute_display_type: 'number',
|
||||
attribute_key: 'order_id',
|
||||
attribute_model: 'conversation_attribute',
|
||||
attribute_values: Array(0),
|
||||
created_at: '2021-11-29T10:20:04.563Z',
|
||||
},
|
||||
],
|
||||
};
|
76
app/javascript/dashboard/helper/specs/preChat.spec.js
Normal file
76
app/javascript/dashboard/helper/specs/preChat.spec.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
getPreChatFields,
|
||||
getFormattedPreChatFields,
|
||||
getCustomFields,
|
||||
} from '../preChat';
|
||||
import inboxFixture from './inboxFixture';
|
||||
|
||||
const { customFields, customAttributes } = inboxFixture;
|
||||
describe('#Pre chat Helpers', () => {
|
||||
describe('getPreChatFields', () => {
|
||||
it('should return correct pre-chat fields form options passed', () => {
|
||||
expect(getPreChatFields({ preChatFormOptions: customFields })).toEqual(
|
||||
customFields
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('getFormattedPreChatFields', () => {
|
||||
it('should return correct custom fields', () => {
|
||||
expect(
|
||||
getFormattedPreChatFields({
|
||||
preChatFields: customFields.pre_chat_fields,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
label: 'Email Address',
|
||||
name: 'emailAddress',
|
||||
placeholder: 'Please enter your email address',
|
||||
type: 'email',
|
||||
field_type: 'standard',
|
||||
|
||||
required: false,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Full Name',
|
||||
name: 'fullName',
|
||||
placeholder: 'Please enter your full name',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
label: 'Phone Number',
|
||||
name: 'phoneNumber',
|
||||
placeholder: 'Please enter your phone number',
|
||||
type: 'text',
|
||||
field_type: 'standard',
|
||||
required: false,
|
||||
enabled: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('getCustomFields', () => {
|
||||
it('should return correct custom fields', () => {
|
||||
expect(
|
||||
getCustomFields({
|
||||
standardFields: { pre_chat_fields: customFields.pre_chat_fields },
|
||||
customAttributes,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
enabled: false,
|
||||
label: 'Order Id',
|
||||
placeholder: 'Order Id',
|
||||
name: 'order_id',
|
||||
required: false,
|
||||
field_type: 'conversation_attribute',
|
||||
type: 'number',
|
||||
values: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -89,7 +89,9 @@
|
|||
"DELETE_MESSAGE": "You need to have atleast one condition to save"
|
||||
},
|
||||
"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": {
|
||||
"ACTIVATION_TITLE": "Activate Automation Rule",
|
||||
|
@ -102,6 +104,13 @@
|
|||
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
|
||||
"CONFIRMATION_LABEL": "Yes",
|
||||
"CANCEL_LABEL": "No"
|
||||
},
|
||||
"ATTACHMENT": {
|
||||
"UPLOAD_ERROR": "Could not upload attachment, Please try again",
|
||||
"LABEL_IDLE": "Upload Attachment",
|
||||
"LABEL_UPLOADING": "Uploading...",
|
||||
"LABEL_UPLOADED": "Succesfully Uploaded",
|
||||
"LABEL_UPLOAD_FAILED": "Upload Failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
"NO_MESSAGES": "No Messages",
|
||||
"NO_CONTENT": "No content available",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text"
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text",
|
||||
"MESSAGE_READ": "Read"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,6 +108,7 @@
|
|||
"GO_TO_CONVERSATION_DASHBOARD": "Go to Conversation Dashboard",
|
||||
"GO_TO_CONTACTS_DASHBOARD": "Go to Contacts Dashboard",
|
||||
"GO_TO_REPORTS_OVERVIEW": "Go to Reports Overview",
|
||||
"GO_TO_CONVERSATION_REPORTS": "Go to Conversation Reports",
|
||||
"GO_TO_AGENT_REPORTS": "Go to Agent Reports",
|
||||
"GO_TO_LABEL_REPORTS": "Go to Label Reports",
|
||||
"GO_TO_INBOX_REPORTS": "Go to Inbox Reports",
|
||||
|
|
|
@ -187,7 +187,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"WHATSAPP": {
|
||||
"TITLE": "WhatsApp Channel",
|
||||
"DESC": "Start supporting your customers via WhatsApp.",
|
||||
"PROVIDERS": {
|
||||
|
@ -211,7 +211,6 @@
|
|||
"PLACEHOLDER": "API key",
|
||||
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||
"ERROR": "Please enter a valid value."
|
||||
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
|
@ -433,6 +432,15 @@
|
|||
},
|
||||
"PRE_CHAT_FORM": {
|
||||
"DESCRIPTION": "Pre chat forms enable you to capture user information before they start conversation with you.",
|
||||
"SET_FIELDS": "Pre chat form fields",
|
||||
"SET_FIELDS_HEADER": {
|
||||
"FIELDS": "Fields",
|
||||
"LABEL": "Label",
|
||||
"PLACE_HOLDER":"Placeholder",
|
||||
"KEY": "Key",
|
||||
"TYPE": "Type",
|
||||
"REQUIRED": "Required"
|
||||
},
|
||||
"ENABLE": {
|
||||
"LABEL": "Enable pre chat form",
|
||||
"OPTIONS": {
|
||||
|
@ -441,7 +449,7 @@
|
|||
}
|
||||
},
|
||||
"PRE_CHAT_MESSAGE": {
|
||||
"LABEL": "Pre Chat Message",
|
||||
"LABEL": "Pre chat message",
|
||||
"PLACEHOLDER": "This message would be visible to the users along with the form"
|
||||
},
|
||||
"REQUIRE_EMAIL": {
|
||||
|
@ -465,11 +473,12 @@
|
|||
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
||||
"CHOOSE": "Choose"
|
||||
},
|
||||
"ALL_DAY":"All-Day"
|
||||
"ALL_DAY": "All-Day"
|
||||
},
|
||||
"IMAP": {
|
||||
"TITLE": "IMAP",
|
||||
"SUBTITLE": "Set your IMAP details",
|
||||
"NOTE_TEXT": "To enable SMTP, please configure IMAP.",
|
||||
"UPDATE": "Update IMAP settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox",
|
||||
"TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
|
||||
|
@ -485,9 +494,9 @@
|
|||
"LABEL": "Port",
|
||||
"PLACE_HOLDER": "Port"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Email",
|
||||
"PLACE_HOLDER": "Email"
|
||||
"LOGIN": {
|
||||
"LABEL": "Login",
|
||||
"PLACE_HOLDER": "Login"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
|
@ -513,9 +522,9 @@
|
|||
"LABEL": "Port",
|
||||
"PLACE_HOLDER": "Port"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Email",
|
||||
"PLACE_HOLDER": "Email"
|
||||
"LOGIN": {
|
||||
"LABEL": "Login",
|
||||
"PLACE_HOLDER": "Login"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
|
@ -528,7 +537,9 @@
|
|||
"ENCRYPTION": "Encryption",
|
||||
"SSL_TLS": "SSL/TLS",
|
||||
"START_TLS": "STARTTLS",
|
||||
"OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode"
|
||||
}
|
||||
"OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode",
|
||||
"AUTH_MECHANISM": "Authentication"
|
||||
},
|
||||
"NOTE": "Note: "
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,29 @@
|
|||
"INTEGRATION_SETTINGS": {
|
||||
"HEADER": "Integrations",
|
||||
"WEBHOOK": {
|
||||
"SUBSCRIBED_EVENTS": "Subscribed Events",
|
||||
"FORM": {
|
||||
"CANCEL": "Cancel",
|
||||
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
|
||||
"SUBSCRIPTIONS": {
|
||||
"LABEL": "Events",
|
||||
"EVENTS": {
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_STATUS_CHANGED": "Conversation Status Changed",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message created",
|
||||
"MESSAGE_UPDATED": "Message updated",
|
||||
"WEBWIDGET_TRIGGERED": "Live chat widget opened by the user"
|
||||
}
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Example: https://example/api/webhook",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"EDIT_SUBMIT": "Update webhook",
|
||||
"ADD_SUBMIT": "Create webhook"
|
||||
},
|
||||
"TITLE": "Webhook",
|
||||
"CONFIGURE": "Configure",
|
||||
"HEADER": "Webhook settings",
|
||||
|
@ -17,35 +40,16 @@
|
|||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"TITLE": "Edit webhook",
|
||||
"CANCEL": "Cancel",
|
||||
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
|
||||
"FORM": {
|
||||
"END_POINT": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Example: https://example/api/webhook",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"SUBMIT": "Edit webhook"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Webhook URL updated successfully",
|
||||
"SUCCESS_MESSAGE": "Webhook configuration updated successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"CANCEL": "Cancel",
|
||||
"TITLE": "Add new webhook",
|
||||
"DESC": "Webhook events provide you the realtime information about what's happening in your Chatwoot account. Please enter a valid URL to configure a callback.",
|
||||
"FORM": {
|
||||
"END_POINT": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Example: https://example/api/webhook",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"SUBMIT": "Create webhook"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Webhook added successfully",
|
||||
"SUCCESS_MESSAGE": "Webhook configuration added successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
|
@ -57,16 +61,16 @@
|
|||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"MESSAGE": "Are you sure to delete the webhook? (%{webhookURL})",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep it"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SLACK": {
|
||||
"HELP_TEXT" : {
|
||||
"HELP_TEXT" : {
|
||||
"TITLE": "Using Slack Integration",
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
"BODY": "<br/><p>Chatwoot will now sync all the incoming conversations into the <b><i>customer-conversations</i></b> channel inside your slack workplace.</p><p>Replying to a conversation thread in <b><i>customer-conversations</i></b> slack channel will create a response back to the customer through chatwoot.</p><p>Start the replies with <b><i>note:</i></b> to create private notes instead of replies.</p><p>If the replier on slack has an agent profile in chatwoot under the same email, the replies will be associated accordingly.</p><p>When the replier doesn't have an associated agent profile, the replies will be made from the bot profile.</p>"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"REPORT": {
|
||||
"HEADER": "Overview",
|
||||
"HEADER": "Conversations",
|
||||
"LOADING_CHART": "Loading chart data...",
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
|
||||
|
@ -20,12 +20,14 @@
|
|||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
|
@ -78,7 +80,8 @@
|
|||
{ "id": 2, "groupBy": "Week" },
|
||||
{ "id": 3, "groupBy": "Month" },
|
||||
{ "id": 4, "groupBy": "Year" }
|
||||
]
|
||||
],
|
||||
"BUSINESS_HOURS": "Business Hours"
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
"HEADER": "Agents Overview",
|
||||
|
@ -102,12 +105,14 @@
|
|||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
|
@ -167,12 +172,14 @@
|
|||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
|
@ -232,12 +239,14 @@
|
|||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
|
@ -297,12 +306,14 @@
|
|||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "First Response Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "First Response Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Resolution Time",
|
||||
"DESC": "( Avg )",
|
||||
"INFO_TEXT": "Total number of conversations used for computation:"
|
||||
"INFO_TEXT": "Total number of conversations used for computation:",
|
||||
"TOOLTIP_TEXT": "Resolution Time is %{metricValue} (based on %{conversationCount} conversations)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
|
@ -370,5 +381,33 @@
|
|||
"TOOLTIP": "Total number of responses / Total number of CSAT survey messages sent * 100"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OVERVIEW_REPORTS": {
|
||||
"HEADER": "Overview",
|
||||
"LIVE": "Live",
|
||||
"ACCOUNT_CONVERSATIONS": {
|
||||
"HEADER": "Open Conversations",
|
||||
"LOADING_MESSAGE": "Conversations Loading...",
|
||||
"TOTAL" : "Total",
|
||||
"UNATTENDED": "Unattended",
|
||||
"UNASSIGNED": "Unassigned"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
"HEADER": "Conversations by agents",
|
||||
"LOADING_MESSAGE": "Agents Loading...",
|
||||
"NO_AGENTS": "There are no conversations by agents",
|
||||
"TABLE_HEADER": {
|
||||
"AGENT": "Agent",
|
||||
"TOTAL": "Total",
|
||||
"UNATTENDED": "Unattended",
|
||||
"STATUS": "Status"
|
||||
}
|
||||
},
|
||||
"AGENT_STATUS": {
|
||||
"HEADER": "Agent status",
|
||||
"ONLINE": "Online",
|
||||
"BUSY": "Busy",
|
||||
"OFFLINE": "Offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"TITLE": "Personal message signature",
|
||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from the platform. Use the rich content editor to create a highly personalised signature.",
|
||||
"BTN_TEXT": "Save message signature",
|
||||
"API_ERROR":"Couldn't save signature! Try again",
|
||||
"API_ERROR": "Couldn't save signature! Try again",
|
||||
"API_SUCCESS": "Signature saved successfully"
|
||||
},
|
||||
"MESSAGE_SIGNATURE": {
|
||||
|
@ -127,6 +127,10 @@
|
|||
"BUTTON_TEXT": "Copy",
|
||||
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
|
||||
},
|
||||
"SHOW_MORE_BLOCK": {
|
||||
"SHOW_MORE": "Show More",
|
||||
"SHOW_LESS": "Show Less"
|
||||
},
|
||||
"FILE_BUBBLE": {
|
||||
"DOWNLOAD": "Download",
|
||||
"UPLOADING": "Uploading..."
|
||||
|
@ -169,7 +173,7 @@
|
|||
"NEW_LABEL": "New label",
|
||||
"NEW_TEAM": "New team",
|
||||
"NEW_INBOX": "New inbox",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REPORTS_CONVERSATION": "Conversations",
|
||||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "Campaigns",
|
||||
"ONGOING": "Ongoing",
|
||||
|
@ -179,7 +183,8 @@
|
|||
"REPORTS_INBOX": "Inbox",
|
||||
"REPORTS_TEAM": "Team",
|
||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||
"BETA": "Beta"
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
"PASSWORD": {
|
||||
"LABEL": "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": {
|
||||
"LABEL": "Confirm Password",
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"SELECT_ALL": "select all agents",
|
||||
"SELECTED_COUNT": "%{selected} out of %{total} agents selected.",
|
||||
"BUTTON_TEXT": "Add agents",
|
||||
"AGENT_VALIDATION_ERROR": "Select atleaset one agent."
|
||||
"AGENT_VALIDATION_ERROR": "Select at least one agent."
|
||||
},
|
||||
|
||||
"FINISH": {
|
||||
|
|
|
@ -15,6 +15,9 @@ export default {
|
|||
chat.private !== true
|
||||
).length;
|
||||
},
|
||||
hasUserReadMessage(createdAt, contactLastSeen) {
|
||||
return !(contactLastSeen - createdAt < 0);
|
||||
},
|
||||
readMessages(m) {
|
||||
return m.messages.filter(
|
||||
chat => chat.created_at * 1000 <= m.agent_last_seen_at * 1000
|
||||
|
|
|
@ -26,4 +26,11 @@ describe('#conversationMixin', () => {
|
|||
conversationMixin.methods.unReadMessages(conversationFixture.conversation)
|
||||
).toEqual(conversationFixture.unReadMessages);
|
||||
});
|
||||
it('should return the user message read flag', () => {
|
||||
const contactLastSeen = 1649856659;
|
||||
const createdAt = 1649859419;
|
||||
expect(
|
||||
conversationMixin.methods.hasUserReadMessage(createdAt, contactLastSeen)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('reportMixin', () => {
|
|||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5);
|
||||
expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
|
||||
'3 Min'
|
||||
'3 Min 18 Sec'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<li>
|
||||
<span class="bullet">❌</span>
|
||||
<span
|
||||
v-html="
|
||||
v-dompurify-html="
|
||||
$t('MERGE_CONTACTS.SUMMARY.DELETE_WARNING', {
|
||||
childContactName,
|
||||
})
|
||||
|
@ -17,7 +17,7 @@
|
|||
<li>
|
||||
<span class="bullet">✅</span>
|
||||
<span
|
||||
v-html="
|
||||
v-dompurify-html="
|
||||
$t('MERGE_CONTACTS.SUMMARY.ATTRIBUTE_WARNING', {
|
||||
childContactName,
|
||||
primaryContactName,
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
<p class="note__content" v-html="formatMessage(note || '')" />
|
||||
<p v-dompurify-html="formatMessage(note || '')" class="note__content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
icon="arrow-chevron-right"
|
||||
@click="submit"
|
||||
/>
|
||||
<p class="accept--terms" v-html="termsLink"></p>
|
||||
<p v-dompurify-html="termsLink" class="accept--terms"></p>
|
||||
</form>
|
||||
<div class="column text-center auth--footer">
|
||||
<span>{{ $t('REGISTER.HAVE_AN_ACCOUNT') }}</span>
|
||||
|
@ -126,7 +126,7 @@ import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
|||
import AuthInput from './components/AuthInput';
|
||||
import AuthHeader from './components/AuthHeader';
|
||||
import AuthSubmitButton from './components/AuthSubmitButton';
|
||||
|
||||
import { isValidPassword } from 'shared/helpers/Validators';
|
||||
export default {
|
||||
components: {
|
||||
AuthInput,
|
||||
|
@ -166,6 +166,7 @@ export default {
|
|||
},
|
||||
password: {
|
||||
required,
|
||||
isValidPassword,
|
||||
minLength: minLength(6),
|
||||
},
|
||||
confirmPassword: {
|
||||
|
@ -195,6 +196,19 @@ export default {
|
|||
}
|
||||
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: {
|
||||
async submit() {
|
||||
|
|
|
@ -92,7 +92,6 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setCurrentAccountId', this.$route.params.accountId);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
bus.$on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
|
|
|
@ -13,6 +13,7 @@ export const ICON_SNOOZE_UNTIL_TOMORRROW = `<svg role="img" class="ninja-icon ni
|
|||
export const ICON_CONVERSATION_DASHBOARD = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M10.55 2.532a2.25 2.25 0 0 1 2.9 0l6.75 5.692c.507.428.8 1.057.8 1.72v9.803a1.75 1.75 0 0 1-1.75 1.75h-3.5a1.75 1.75 0 0 1-1.75-1.75v-5.5a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25v5.5a1.75 1.75 0 0 1-1.75 1.75h-3.5A1.75 1.75 0 0 1 3 19.747V9.944c0-.663.293-1.292.8-1.72l6.75-5.692zm1.933 1.147a.75.75 0 0 0-.966 0L4.767 9.37a.75.75 0 0 0-.267.573v9.803c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-5.5c0-.967.784-1.75 1.75-1.75h3.5c.966 0 1.75.783 1.75 1.75v5.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25V9.944a.75.75 0 0 0-.267-.573l-6.75-5.692z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_CONTACT_DASHBOARD = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438c-1.57 1.834-3.957 2.739-7.102 2.739c-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461c1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75zM12 2.004a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_REPORTS_OVERVIEW = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M16.749 2h4.554l.1.014l.099.028l.06.026c.08.034.153.085.219.15l.04.044l.044.057l.054.09l.039.09l.019.064l.014.064l.009.095v4.532a.75.75 0 0 1-1.493.102l-.007-.102V4.559l-6.44 6.44a.75.75 0 0 1-.976.073L13 11L9.97 8.09l-5.69 5.689a.75.75 0 0 1-1.133-.977l.073-.084l6.22-6.22a.75.75 0 0 1 .976-.072l.084.072l3.03 2.91L19.438 3.5h-2.69a.75.75 0 0 1-.742-.648l-.007-.102a.75.75 0 0 1 .648-.743L16.75 2zM3.75 17a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5a.75.75 0 0 1 .75-.75zm5.75-3.25a.75.75 0 0 0-1.5 0v7.5a.75.75 0 0 0 1.5 0v-7.5zM13.75 15a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0v-5.5a.75.75 0 0 1 .75-.75zm5.75-4.25a.75.75 0 0 0-1.5 0v10.5a.75.75 0 0 0 1.5 0v-10.5z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_CONVERSATION_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5ZM8.75 13h4.498a.75.75 0 0 1 .102 1.493l-.102.007H8.75a.75.75 0 0 1-.102-1.493L8.75 13h4.498H8.75Zm0-3.5h6.505a.75.75 0 0 1 .101 1.493l-.101.007H8.75a.75.75 0 0 1-.102-1.493L8.75 9.5h6.505H8.75Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AGENT_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M4 13.999L13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14zM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78zM4 15.499l-.1.01a.51.51 0 0 0-.254.136a.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242c.826.445 2.003.714 3.266.753l.317.005l.317-.005c1.263-.039 2.439-.308 3.266-.753c.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001zM8.5 3a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9zm9 2a3.5 3.5 0 1 1 0 7a3.5 3.5 0 0 1 0-7zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3s3-1.346 3-3s-1.346-3-3-3zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2s2-.897 2-2s-.897-2-2-2z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_LABEL_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75zM17 5.502a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_INBOX_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3h11.5h-11.5zM4.5 14.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188H4.5v3.25v-3.25zm13.25-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5z" fill="currentColor"></path></g></svg>`;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
ICON_REPORTS_OVERVIEW,
|
||||
ICON_TEAM_REPORTS,
|
||||
ICON_USER_PROFILE,
|
||||
ICON_CONVERSATION_REPORTS,
|
||||
} from './CommandBarIcons';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
|
@ -41,6 +42,14 @@ const GO_TO_COMMANDS = [
|
|||
path: accountId => `accounts/${accountId}/reports/overview`,
|
||||
role: ['administrator'],
|
||||
},
|
||||
{
|
||||
id: 'open_conversation_reports',
|
||||
section: 'COMMAND_BAR.SECTIONS.REPORTS',
|
||||
title: 'COMMAND_BAR.COMMANDS.GO_TO_CONVERSATION_REPORTS',
|
||||
icon: ICON_CONVERSATION_REPORTS,
|
||||
path: accountId => `accounts/${accountId}/reports/conversation`,
|
||||
role: ['administrator'],
|
||||
},
|
||||
{
|
||||
id: 'open_agent_reports',
|
||||
section: 'COMMAND_BAR.SECTIONS.REPORTS',
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
{{ attribute }}
|
||||
</div>
|
||||
<div>
|
||||
<span v-html="valueWithLink(customAttributes[attribute])"></span>
|
||||
<span
|
||||
v-dompurify-html="valueWithLink(customAttributes[attribute])"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!listOfAttributes.length">
|
||||
|
|
|
@ -59,6 +59,13 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="columns">
|
||||
<div class="canned-response">
|
||||
<canned-response
|
||||
v-if="showCannedResponseMenu && hasSlashCommand"
|
||||
:search-key="cannedResponseSearchKey"
|
||||
@click="replaceTextWithCannedResponse"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isAnEmailInbox || isAnWebWidgetInbox">
|
||||
<label>
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
||||
|
@ -113,6 +120,7 @@ import { mapGetters } from 'vuex';
|
|||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead';
|
||||
import CannedResponse from 'dashboard/components/widgets/conversation/CannedResponse.vue';
|
||||
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||
|
@ -124,6 +132,7 @@ export default {
|
|||
Thumbnail,
|
||||
WootMessageEditor,
|
||||
ReplyEmailHead,
|
||||
CannedResponse,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
|
@ -141,6 +150,8 @@ export default {
|
|||
name: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
showCannedResponseMenu: false,
|
||||
cannedResponseSearchKey: '',
|
||||
selectedInbox: '',
|
||||
bccEmails: '',
|
||||
ccEmails: '',
|
||||
|
@ -211,6 +222,20 @@ export default {
|
|||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
message(value) {
|
||||
this.hasSlashCommand = value[0] === '/';
|
||||
const hasNextWord = value.includes(' ');
|
||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
this.cannedResponseSearchKey = value.substr(1, value.length);
|
||||
this.showCannedResponseMenu = true;
|
||||
} else {
|
||||
this.cannedResponseSearchKey = '';
|
||||
this.showCannedResponseMenu = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
this.$emit('cancel');
|
||||
|
@ -244,6 +269,11 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
replaceTextWithCannedResponse(message) {
|
||||
setTimeout(() => {
|
||||
this.message = message;
|
||||
}, 50);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -251,9 +281,15 @@ export default {
|
|||
<style scoped lang="scss">
|
||||
.conversation--form {
|
||||
padding: var(--space-normal) var(--space-large) var(--space-large);
|
||||
}
|
||||
|
||||
.columns {
|
||||
padding: 0 var(--space-smaller);
|
||||
.canned-response {
|
||||
position: relative;
|
||||
top: var(--space-medium);
|
||||
|
||||
::v-deep .mention--box {
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<span class="timestamp">{{ readableTime }} </span>
|
||||
</div>
|
||||
<p class="message-content" v-html="prepareContent(content)"></p>
|
||||
<p v-dompurify-html="prepareContent(content)" class="message-content"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h2 class="page-sub-title">
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p class="small-12 column" v-html="headerContent"></p>
|
||||
<p v-dompurify-html="headerContent" class="small-12 column"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
</div>
|
||||
<div class="small-4 columns">
|
||||
<span
|
||||
v-html="
|
||||
v-dompurify-html="
|
||||
useInstallationName(
|
||||
$t('AGENT_MGMT.SIDEBAR_TXT'),
|
||||
globalConfig.installationName
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="small-4 columns">
|
||||
<span v-html="$t('ATTRIBUTES_MGMT.SIDEBAR_TXT')"></span>
|
||||
<span v-dompurify-html="$t('ATTRIBUTES_MGMT.SIDEBAR_TXT')"></span>
|
||||
</div>
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<edit-attribute
|
||||
|
|
|
@ -192,10 +192,11 @@ export default {
|
|||
$each: {
|
||||
action_params: {
|
||||
required: requiredIf(prop => {
|
||||
if (prop.action_name === 'send_email_to_team') return true;
|
||||
return !(
|
||||
prop.action_name === 'mute_conversation' ||
|
||||
prop.action_name === 'snooze_convresation' ||
|
||||
prop.action_name === 'resolve_convresation'
|
||||
prop.action_name === 'snooze_conversation' ||
|
||||
prop.action_name === 'resolve_conversation'
|
||||
);
|
||||
}),
|
||||
},
|
||||
|
@ -361,6 +362,7 @@ export default {
|
|||
getActionDropdownValues(type) {
|
||||
switch (type) {
|
||||
case 'assign_team':
|
||||
case 'send_email_to_team':
|
||||
return this.$store.getters['teams/getTeams'];
|
||||
case 'add_label':
|
||||
return this.$store.getters['labels/getLabels'].map(i => {
|
||||
|
@ -443,6 +445,8 @@ export default {
|
|||
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;
|
||||
|
|
|
@ -101,6 +101,12 @@
|
|||
showActionInput(automation.actions[i].action_name)
|
||||
"
|
||||
:v="$v.automation.actions.$each[i]"
|
||||
:initial-file-name="
|
||||
getFileName(
|
||||
automation.actions[i].action_params[0],
|
||||
automation.actions[i].action_name
|
||||
)
|
||||
"
|
||||
@removeAction="removeAction(i)"
|
||||
/>
|
||||
<div class="filter-actions">
|
||||
|
@ -198,8 +204,8 @@ export default {
|
|||
required: requiredIf(prop => {
|
||||
return !(
|
||||
prop.action_name === 'mute_conversation' ||
|
||||
prop.action_name === 'snooze_convresation' ||
|
||||
prop.action_name === 'resolve_convresation'
|
||||
prop.action_name === 'snooze_conversation' ||
|
||||
prop.action_name === 'resolve_conversation'
|
||||
);
|
||||
}),
|
||||
},
|
||||
|
@ -360,6 +366,7 @@ export default {
|
|||
getActionDropdownValues(type) {
|
||||
switch (type) {
|
||||
case 'assign_team':
|
||||
case 'send_email_to_team':
|
||||
return this.$store.getters['teams/getTeams'];
|
||||
case 'add_label':
|
||||
return this.$store.getters['labels/getLabels'].map(i => {
|
||||
|
@ -475,6 +482,15 @@ export default {
|
|||
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 {
|
||||
|
@ -489,12 +505,23 @@ export default {
|
|||
};
|
||||
},
|
||||
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;
|
||||
},
|
||||
getFileName(id, actionType) {
|
||||
if (!id) return '';
|
||||
if (actionType === 'send_attachment') {
|
||||
const file = this.automation.files.find(item => item.blob_id === id);
|
||||
// replace `blob_id.toString()` with file name once api is fixed.
|
||||
if (file) return file.filename.toString();
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -34,19 +34,10 @@
|
|||
<td>{{ automation.name }}</td>
|
||||
<td>{{ automation.description }}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
: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>
|
||||
<woot-switch
|
||||
:value="automation.active"
|
||||
@input="toggleAutomation(automation, automation.active)"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ readableTime(automation.created_on) }}</td>
|
||||
<td class="button-wrapper">
|
||||
|
@ -90,7 +81,7 @@
|
|||
</div>
|
||||
|
||||
<div class="small-4 columns">
|
||||
<span v-html="$t('AUTOMATION.SIDEBAR_TXT')"></span>
|
||||
<span v-dompurify-html="$t('AUTOMATION.SIDEBAR_TXT')"></span>
|
||||
</div>
|
||||
</div>
|
||||
<woot-modal
|
||||
|
@ -238,7 +229,6 @@ export default {
|
|||
mode === 'EDIT'
|
||||
? this.$t('AUTOMATION.EDIT.API.SUCCESS_MESSAGE')
|
||||
: this.$t('AUTOMATION.ADD.API.SUCCESS_MESSAGE');
|
||||
|
||||
await await this.$store.dispatch(action, payload);
|
||||
this.showAlert(this.$t(successMessage));
|
||||
this.hideAddPopup();
|
||||
|
@ -263,7 +253,7 @@ export default {
|
|||
: this.$t('AUTOMATION.TOGGLE.ACTIVATION_DESCRIPTION', {
|
||||
automationName: automation.name,
|
||||
});
|
||||
// Check if uses confirms to proceed
|
||||
// Check if user confirms to proceed
|
||||
const ok = await this.$refs.confirmDialog.showConfirmation();
|
||||
if (ok) {
|
||||
await await this.$store.dispatch('automations/update', {
|
||||
|
@ -290,41 +280,4 @@ export default {
|
|||
.automation__status-checkbox {
|
||||
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>
|
||||
|
|
|
@ -64,6 +64,13 @@ export const AUTOMATIONS = {
|
|||
inputType: 'plain_text',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
@ -76,11 +83,16 @@ export const AUTOMATIONS = {
|
|||
name: 'Add a label',
|
||||
attributeI18nKey: 'ADD_LABEL',
|
||||
},
|
||||
// {
|
||||
// key: 'send_email_to_team',
|
||||
// name: 'Send an email to team',
|
||||
// attributeI18nKey: 'SEND_MESSAGE',
|
||||
// },
|
||||
{
|
||||
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',
|
||||
|
@ -92,12 +104,13 @@ export const AUTOMATIONS = {
|
|||
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'snooze_convresation',
|
||||
key: 'snooze_conversation',
|
||||
name: 'Snooze conversation',
|
||||
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||
},
|
||||
|
||||
{
|
||||
key: 'resolve_convresation',
|
||||
key: 'resolve_conversation',
|
||||
name: 'Resolve conversation',
|
||||
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||
},
|
||||
|
@ -106,6 +119,11 @@ export const AUTOMATIONS = {
|
|||
name: 'Send Webhook Event',
|
||||
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||
},
|
||||
{
|
||||
key: 'send_attachment',
|
||||
name: 'Send Attachment',
|
||||
attributeI18nKey: 'SEND_ATTACHMENT',
|
||||
},
|
||||
],
|
||||
},
|
||||
conversation_created: {
|
||||
|
@ -132,12 +150,19 @@ export const AUTOMATIONS = {
|
|||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
{
|
||||
key: 'referrer',
|
||||
key: 'referer',
|
||||
name: 'Referrer Link',
|
||||
attributeI18nKey: 'REFERER_LINK',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: OPERATOR_TYPES_2,
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
@ -150,11 +175,16 @@ export const AUTOMATIONS = {
|
|||
name: 'Assign an agent',
|
||||
attributeI18nKey: 'ASSIGN_AGENT',
|
||||
},
|
||||
// {
|
||||
// key: 'send_email_to_team',
|
||||
// name: 'Send an email to team',
|
||||
// attributeI18nKey: 'SEND_MESSAGE',
|
||||
// },
|
||||
{
|
||||
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',
|
||||
|
@ -166,12 +196,12 @@ export const AUTOMATIONS = {
|
|||
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'snooze_convresation',
|
||||
key: 'snooze_conversation',
|
||||
name: 'Snooze conversation',
|
||||
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'resolve_convresation',
|
||||
key: 'resolve_conversation',
|
||||
name: 'Resolve conversation',
|
||||
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||
},
|
||||
|
@ -180,6 +210,11 @@ export const AUTOMATIONS = {
|
|||
name: 'Send Webhook Event',
|
||||
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||
},
|
||||
{
|
||||
key: 'send_attachment',
|
||||
name: 'Send Attachment',
|
||||
attributeI18nKey: 'SEND_ATTACHMENT',
|
||||
},
|
||||
],
|
||||
},
|
||||
conversation_updated: {
|
||||
|
@ -226,6 +261,13 @@ export const AUTOMATIONS = {
|
|||
inputType: 'search_select',
|
||||
filterOperators: OPERATOR_TYPES_3,
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: OPERATOR_TYPES_1,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
|
@ -238,11 +280,16 @@ export const AUTOMATIONS = {
|
|||
name: 'Assign an agent',
|
||||
attributeI18nKey: 'ASSIGN_AGENT',
|
||||
},
|
||||
// {
|
||||
// key: 'send_email_to_team',
|
||||
// name: 'Send an email to team',
|
||||
// attributeI18nKey: 'SEND_MESSAGE',
|
||||
// },
|
||||
{
|
||||
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',
|
||||
|
@ -254,12 +301,12 @@ export const AUTOMATIONS = {
|
|||
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'snooze_convresation',
|
||||
key: 'snooze_conversation',
|
||||
name: 'Snooze conversation',
|
||||
attributeI18nKey: 'MUTE_CONVERSATION',
|
||||
},
|
||||
{
|
||||
key: 'resolve_convresation',
|
||||
key: 'resolve_conversation',
|
||||
name: 'Resolve conversation',
|
||||
attributeI18nKey: 'RESOLVE_CONVERSATION',
|
||||
},
|
||||
|
@ -268,6 +315,11 @@ export const AUTOMATIONS = {
|
|||
name: 'Send Webhook Event',
|
||||
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
|
||||
},
|
||||
{
|
||||
key: 'send_attachment',
|
||||
name: 'Send Attachment',
|
||||
attributeI18nKey: 'SEND_ATTACHMENT',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -298,11 +350,11 @@ export const AUTOMATION_ACTION_TYPES = [
|
|||
label: 'Add a label',
|
||||
inputType: 'multi_select',
|
||||
},
|
||||
// {
|
||||
// key: 'send_email_to_team',
|
||||
// label: 'Send an email to team',
|
||||
// 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',
|
||||
|
@ -314,12 +366,12 @@ export const AUTOMATION_ACTION_TYPES = [
|
|||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'snooze_convresation',
|
||||
key: 'snooze_conversation',
|
||||
label: 'Snooze conversation',
|
||||
inputType: null,
|
||||
},
|
||||
{
|
||||
key: 'resolve_convresation',
|
||||
key: 'resolve_conversation',
|
||||
label: 'Resolve conversation',
|
||||
inputType: null,
|
||||
},
|
||||
|
@ -328,4 +380,14 @@ export const AUTOMATION_ACTION_TYPES = [
|
|||
label: 'Send Webhook Event',
|
||||
inputType: 'url',
|
||||
},
|
||||
{
|
||||
key: 'send_attachment',
|
||||
label: 'Send Attachment',
|
||||
inputType: 'attachment',
|
||||
},
|
||||
{
|
||||
key: 'send_message',
|
||||
label: 'Send a message',
|
||||
inputType: 'textarea',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
</div>
|
||||
|
||||
<div class="small-4 columns">
|
||||
<span v-html="$t('CANNED_MGMT.SIDEBAR_TXT')"></span>
|
||||
<span v-dompurify-html="$t('CANNED_MGMT.SIDEBAR_TXT')"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add Agent -->
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<settings-section
|
||||
:title="$t('INBOX_MGMT.IMAP.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.IMAP.SUBTITLE')"
|
||||
:note="$t('INBOX_MGMT.IMAP.NOTE_TEXT')"
|
||||
>
|
||||
<form @submit.prevent="updateInbox">
|
||||
<label for="toggle-imap-enable">
|
||||
|
@ -33,12 +34,12 @@
|
|||
@blur="$v.port.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="email"
|
||||
:class="{ error: $v.email.$error }"
|
||||
v-model="login"
|
||||
:class="{ error: $v.login.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.IMAP.EMAIL.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.IMAP.EMAIL.PLACE_HOLDER')"
|
||||
@blur="$v.email.$touch"
|
||||
:label="$t('INBOX_MGMT.IMAP.LOGIN.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.IMAP.LOGIN.PLACE_HOLDER')"
|
||||
@blur="$v.login.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="password"
|
||||
|
@ -72,7 +73,7 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import SettingsSection from 'dashboard/components/SettingsSection';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -90,7 +91,7 @@ export default {
|
|||
isIMAPEnabled: false,
|
||||
address: '',
|
||||
port: '',
|
||||
email: '',
|
||||
login: '',
|
||||
password: '',
|
||||
isSSLEnabled: true,
|
||||
};
|
||||
|
@ -98,7 +99,7 @@ export default {
|
|||
validations: {
|
||||
address: { required },
|
||||
port: { required, minLength: minLength(2) },
|
||||
email: { required, email },
|
||||
login: { required },
|
||||
password: { required },
|
||||
},
|
||||
computed: {
|
||||
|
@ -118,28 +119,28 @@ export default {
|
|||
imap_enabled,
|
||||
imap_address,
|
||||
imap_port,
|
||||
imap_email,
|
||||
imap_login,
|
||||
imap_password,
|
||||
imap_enable_ssl,
|
||||
} = this.inbox;
|
||||
this.isIMAPEnabled = imap_enabled;
|
||||
this.address = imap_address;
|
||||
this.port = imap_port;
|
||||
this.email = imap_email;
|
||||
this.login = imap_login;
|
||||
this.password = imap_password;
|
||||
this.isSSLEnabled = imap_enable_ssl;
|
||||
},
|
||||
async updateInbox() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const payload = {
|
||||
let payload = {
|
||||
id: this.inbox.id,
|
||||
formData: false,
|
||||
channel: {
|
||||
imap_enabled: this.isIMAPEnabled,
|
||||
imap_address: this.address,
|
||||
imap_port: this.port,
|
||||
imap_email: this.email,
|
||||
imap_login: this.login,
|
||||
imap_password: this.password,
|
||||
imap_enable_ssl: this.isSSLEnabled,
|
||||
imap_inbox_synced_at: this.isIMAPEnabled
|
||||
|
@ -147,6 +148,11 @@ export default {
|
|||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (!this.isIMAPEnabled) {
|
||||
payload.channel.smtp_enabled = false;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('inboxes/updateInboxIMAP', payload);
|
||||
this.showAlert(this.$t('INBOX_MGMT.IMAP.EDIT.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
|
||||
<div class="small-4 columns">
|
||||
<span
|
||||
v-html="
|
||||
v-dompurify-html="
|
||||
useInstallationName(
|
||||
$t('INBOX_MGMT.SIDEBAR_TXT'),
|
||||
globalConfig.installationName
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<draggable v-model="preChatFields" tag="tbody">
|
||||
<tr v-for="(item, index) in preChatFields" :key="index">
|
||||
<td class="pre-chat-field"><fluent-icon icon="drag" /></td>
|
||||
<td class="pre-chat-field">
|
||||
<woot-switch
|
||||
:value="item['enabled']"
|
||||
@input="handlePreChatFieldOptions($event, 'enabled', item)"
|
||||
/>
|
||||
</td>
|
||||
<td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }">
|
||||
{{ item.name }}
|
||||
</td>
|
||||
<td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }">
|
||||
{{ item.type }}
|
||||
</td>
|
||||
<td class="pre-chat-field">
|
||||
<input
|
||||
v-model="item['required']"
|
||||
type="checkbox"
|
||||
:value="`${item.name}-required`"
|
||||
:disabled="!item['enabled']"
|
||||
@click="handlePreChatFieldOptions($event, 'required', item)"
|
||||
/>
|
||||
</td>
|
||||
<td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }">
|
||||
<input
|
||||
v-model.trim="item.label"
|
||||
type="text"
|
||||
:disabled="isFieldEditable(item)"
|
||||
/>
|
||||
</td>
|
||||
<td class="pre-chat-field" :class="{ 'disabled-text': !item['enabled'] }">
|
||||
<input
|
||||
v-model.trim="item.placeholder"
|
||||
type="text"
|
||||
:disabled="isFieldEditable(item)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable';
|
||||
import { standardFieldKeys } from 'dashboard/helper/preChat';
|
||||
export default {
|
||||
components: { draggable },
|
||||
props: {
|
||||
preChatFields: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
handlePreChatFieldOptions: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isFieldEditable(item) {
|
||||
return !!standardFieldKeys[item.name] || !item.enabled;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.pre-chat-field {
|
||||
padding: var(--space-normal) var(--space-small);
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.disabled-text {
|
||||
color: var(--s-500);
|
||||
}
|
||||
|
||||
table {
|
||||
thead th {
|
||||
text-transform: none;
|
||||
}
|
||||
input {
|
||||
font-size: var(--font-size-small);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="settings--content">
|
||||
<div class="prechat--title">
|
||||
<div class="pre-chat--title">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.DESCRIPTION') }}
|
||||
</div>
|
||||
<form class="medium-6" @submit.prevent="updateInbox">
|
||||
<label class="medium-9 columns">
|
||||
<form @submit.prevent="updateInbox">
|
||||
<label class="medium-3 columns">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.ENABLE.LABEL') }}
|
||||
<select v-model="preChatFormEnabled">
|
||||
<option :value="true">
|
||||
|
@ -15,28 +15,55 @@
|
|||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="medium-9">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="preChatMessage"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
v-model="preChatFieldOptions"
|
||||
type="checkbox"
|
||||
value="requireEmail"
|
||||
@input="handlePreChatFieldOptions"
|
||||
/>
|
||||
<label for="requireEmail">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.REQUIRE_EMAIL.LABEL') }}
|
||||
<div v-if="preChatFormEnabled">
|
||||
<label class="medium-3 columns">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="preChatMessage"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label class="medium-8 columns">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS') }}
|
||||
<table class="table table-striped w-full">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.KEY') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.TYPE') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{
|
||||
$t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.REQUIRED')
|
||||
}}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.LABEL') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.PLACE_HOLDER'
|
||||
)
|
||||
}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<pre-chat-fields
|
||||
:pre-chat-fields="preChatFields"
|
||||
:handle-pre-chat-field-options="handlePreChatFieldOptions"
|
||||
/>
|
||||
</table>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||
:loading="uiFlags.isUpdatingInbox"
|
||||
|
@ -47,8 +74,13 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import PreChatFields from './PreChatFields.vue';
|
||||
import { getPreChatFields, standardFieldKeys } from 'dashboard/helper/preChat';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PreChatFields,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
|
@ -60,11 +92,21 @@ export default {
|
|||
return {
|
||||
preChatFormEnabled: false,
|
||||
preChatMessage: '',
|
||||
preChatFieldOptions: [],
|
||||
preChatFields: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
...mapGetters({
|
||||
uiFlags: 'inboxes/getUIFlags',
|
||||
customAttributes: 'attributes/getAttributes',
|
||||
}),
|
||||
preChatFieldOptions() {
|
||||
const { pre_chat_form_options: preChatFormOptions } = this.inbox;
|
||||
return getPreChatFields({
|
||||
preChatFormOptions,
|
||||
customAttributes: this.customAttributes,
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inbox() {
|
||||
|
@ -76,25 +118,26 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
setDefaults() {
|
||||
const {
|
||||
pre_chat_form_enabled: preChatFormEnabled,
|
||||
pre_chat_form_options: preChatFormOptions,
|
||||
} = this.inbox;
|
||||
const { pre_chat_form_enabled: preChatFormEnabled } = this.inbox;
|
||||
this.preChatFormEnabled = preChatFormEnabled;
|
||||
const { pre_chat_message: preChatMessage, require_email: requireEmail } =
|
||||
preChatFormOptions || {};
|
||||
const {
|
||||
pre_chat_message: preChatMessage,
|
||||
pre_chat_fields: preChatFields,
|
||||
} = this.preChatFieldOptions || {};
|
||||
this.preChatMessage = preChatMessage;
|
||||
if (requireEmail) {
|
||||
this.preChatFieldOptions = ['requireEmail'];
|
||||
}
|
||||
this.preChatFields = preChatFields;
|
||||
},
|
||||
handlePreChatFieldOptions(event) {
|
||||
if (this.preChatFieldOptions.includes(event.target.value)) {
|
||||
this.preChatFieldOptions = [];
|
||||
} else {
|
||||
this.preChatFieldOptions = [event.target.value];
|
||||
}
|
||||
isFieldEditable(item) {
|
||||
return !!standardFieldKeys[item.name] || !item.enabled;
|
||||
},
|
||||
handlePreChatFieldOptions(event, type, item) {
|
||||
this.preChatFields.forEach((field, index) => {
|
||||
if (field.name === item.name) {
|
||||
this.preChatFields[index][type] = !item[type];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async updateInbox() {
|
||||
try {
|
||||
const payload = {
|
||||
|
@ -104,7 +147,7 @@ export default {
|
|||
pre_chat_form_enabled: this.preChatFormEnabled,
|
||||
pre_chat_form_options: {
|
||||
pre_chat_message: this.preChatMessage,
|
||||
require_email: this.preChatFieldOptions.includes('requireEmail'),
|
||||
pre_chat_fields: this.preChatFields,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -117,12 +160,11 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.settings--content {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
|
||||
.prechat--title {
|
||||
.pre-chat--title {
|
||||
margin: var(--space-medium) 0 var(--space-slab);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -422,7 +422,7 @@
|
|||
</settings-section>
|
||||
</div>
|
||||
<imap-settings :inbox="inbox" />
|
||||
<smtp-settings :inbox="inbox" />
|
||||
<smtp-settings v-if="inbox.imap_enabled" :inbox="inbox" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedTabKey === 'preChatForm'">
|
||||
|
|
|
@ -33,12 +33,12 @@
|
|||
@blur="$v.port.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="email"
|
||||
:class="{ error: $v.email.$error }"
|
||||
v-model="login"
|
||||
:class="{ error: $v.login.$error }"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.SMTP.EMAIL.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.SMTP.EMAIL.PLACE_HOLDER')"
|
||||
@blur="$v.email.$touch"
|
||||
:label="$t('INBOX_MGMT.SMTP.LOGIN.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.SMTP.LOGIN.PLACE_HOLDER')"
|
||||
@blur="$v.login.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="password"
|
||||
|
@ -69,6 +69,13 @@
|
|||
:options="openSSLVerifyModes"
|
||||
:action="handleSSLModeChange"
|
||||
/>
|
||||
<single-select-dropdown
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.SMTP.AUTH_MECHANISM')"
|
||||
:selected="authMechanism"
|
||||
:options="authMechanisms"
|
||||
:action="handleAuthMechanismChange"
|
||||
/>
|
||||
</div>
|
||||
<woot-submit-button
|
||||
:button-text="$t('INBOX_MGMT.SMTP.UPDATE')"
|
||||
|
@ -84,7 +91,7 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import SettingsSection from 'dashboard/components/SettingsSection';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import InputRadioGroup from './components/InputRadioGroup';
|
||||
import SingleSelectDropdown from './components/SingleSelectDropdown';
|
||||
|
||||
|
@ -106,12 +113,13 @@ export default {
|
|||
isSMTPEnabled: false,
|
||||
address: '',
|
||||
port: '',
|
||||
email: '',
|
||||
login: '',
|
||||
password: '',
|
||||
domain: '',
|
||||
ssl: false,
|
||||
starttls: true,
|
||||
openSSLVerifyMode: 'none',
|
||||
authMechanism: 'login',
|
||||
encryptionProtocols: [
|
||||
{ id: 'ssl', title: 'SSL/TLS', checked: false },
|
||||
{ id: 'starttls', title: 'STARTTLS', checked: true },
|
||||
|
@ -122,6 +130,15 @@ export default {
|
|||
{ key: 3, value: 'client_once' },
|
||||
{ key: 4, value: 'fail_if_no_peer_cert' },
|
||||
],
|
||||
authMechanisms: [
|
||||
{ key: 1, value: 'plain' },
|
||||
{ key: 2, value: 'login' },
|
||||
{ key: 3, value: 'cram-md5' },
|
||||
{ key: 4, value: 'xoauth' },
|
||||
{ key: 5, value: 'xoauth2' },
|
||||
{ key: 6, value: 'ntlm' },
|
||||
{ key: 7, value: 'gssapi' },
|
||||
],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
|
@ -130,7 +147,7 @@ export default {
|
|||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
email: { required, email },
|
||||
login: { required },
|
||||
password: { required },
|
||||
domain: { required },
|
||||
},
|
||||
|
@ -151,22 +168,24 @@ export default {
|
|||
smtp_enabled,
|
||||
smtp_address,
|
||||
smtp_port,
|
||||
smtp_email,
|
||||
smtp_login,
|
||||
smtp_password,
|
||||
smtp_domain,
|
||||
smtp_enable_starttls_auto,
|
||||
smtp_enable_ssl_tls,
|
||||
smtp_openssl_verify_mode,
|
||||
smtp_authentication,
|
||||
} = this.inbox;
|
||||
this.isSMTPEnabled = smtp_enabled;
|
||||
this.address = smtp_address;
|
||||
this.port = smtp_port;
|
||||
this.email = smtp_email;
|
||||
this.login = smtp_login;
|
||||
this.password = smtp_password;
|
||||
this.domain = smtp_domain;
|
||||
this.starttls = smtp_enable_starttls_auto;
|
||||
this.ssl = smtp_enable_ssl_tls;
|
||||
this.openSSLVerifyMode = smtp_openssl_verify_mode;
|
||||
this.authMechanism = smtp_authentication;
|
||||
|
||||
this.encryptionProtocols = [
|
||||
{ id: 'ssl', title: 'SSL/TLS', checked: smtp_enable_ssl_tls },
|
||||
|
@ -189,6 +208,9 @@ export default {
|
|||
handleSSLModeChange(mode) {
|
||||
this.openSSLVerifyMode = mode;
|
||||
},
|
||||
handleAuthMechanismChange(mode) {
|
||||
this.authMechanism = mode;
|
||||
},
|
||||
async updateInbox() {
|
||||
try {
|
||||
const payload = {
|
||||
|
@ -198,12 +220,13 @@ export default {
|
|||
smtp_enabled: this.isSMTPEnabled,
|
||||
smtp_address: this.address,
|
||||
smtp_port: this.port,
|
||||
smtp_email: this.email,
|
||||
smtp_login: this.login,
|
||||
smtp_password: this.password,
|
||||
smtp_domain: this.domain,
|
||||
smtp_enable_ssl_tls: this.ssl,
|
||||
smtp_enable_starttls_auto: this.starttls,
|
||||
smtp_openssl_verify_mode: this.openSSLVerifyMode,
|
||||
smtp_authentication: this.authMechanism,
|
||||
},
|
||||
};
|
||||
await this.$store.dispatch('inboxes/updateInboxSMTP', payload);
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<b>{{ integration.name }}</b>
|
||||
</p>
|
||||
<p
|
||||
v-html="
|
||||
v-dompurify-html="
|
||||
$t(
|
||||
`INTEGRATION_APPS.SIDEBAR_DESCRIPTION.${integration.name.toUpperCase()}`,
|
||||
{ installationName: globalConfig.installationName }
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')"
|
||||
/>
|
||||
<form class="row" @submit.prevent="editWebhook">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.endPoint.$error }">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.LABEL') }}
|
||||
<input
|
||||
v-model.trim="endPoint"
|
||||
type="text"
|
||||
name="endPoint"
|
||||
:placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.PLACEHOLDER')
|
||||
"
|
||||
@input="$v.endPoint.$touch"
|
||||
/>
|
||||
<span v-if="$v.endPoint.$error" class="message">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-button
|
||||
:is-disabled="
|
||||
$v.endPoint.$invalid || uiFlags.updatingItem || endPoint === url
|
||||
"
|
||||
:is-loading="uiFlags.updatingItem"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
alertMessage: '',
|
||||
endPoint: this.url,
|
||||
webhookId: this.id,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
endPoint: {
|
||||
required,
|
||||
minLength: minLength(7),
|
||||
url,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'webhooks/getUIFlags' }),
|
||||
},
|
||||
methods: {
|
||||
resetForm() {
|
||||
this.endPoint = '';
|
||||
this.$v.endPoint.$reset();
|
||||
},
|
||||
async editWebhook() {
|
||||
try {
|
||||
await this.$store.dispatch('webhooks/update', {
|
||||
webhook: { url: this.endPoint },
|
||||
id: this.webhookId,
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE'
|
||||
);
|
||||
this.resetForm();
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error.response.data.message ||
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -4,7 +4,9 @@
|
|||
<div class="integration--description">
|
||||
<h5>{{ $t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.TITLE') }}</h5>
|
||||
<p>
|
||||
<span v-html="$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.BODY')"></span>
|
||||
<span
|
||||
v-dompurify-html="$t('INTEGRATION_SETTINGS.SLACK.HELP_TEXT.BODY')"
|
||||
></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose" :close-on-backdrop-click="false">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')"
|
||||
:header-content="
|
||||
useInstallationName(
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.DESC'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
/>
|
||||
<form class="row" @submit.prevent="addWebhook">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.endPoint.$error }">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.LABEL') }}
|
||||
<input
|
||||
v-model.trim="endPoint"
|
||||
type="text"
|
||||
name="endPoint"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
@input="$v.endPoint.$touch"
|
||||
/>
|
||||
<span v-if="$v.endPoint.$error" class="message">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-button
|
||||
:disabled="$v.endPoint.$invalid || addWebHook.showLoading"
|
||||
:is-loading="addWebHook.showLoading"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.ADD.CANCEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import Modal from '../../../../components/Modal';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
endPoint: '',
|
||||
addWebHook: {
|
||||
showAlert: false,
|
||||
showLoading: false,
|
||||
},
|
||||
show: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
},
|
||||
validations: {
|
||||
endPoint: {
|
||||
required,
|
||||
minLength: minLength(7),
|
||||
url,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetForm() {
|
||||
this.endPoint = '';
|
||||
this.$v.endPoint.$reset();
|
||||
},
|
||||
async addWebhook() {
|
||||
this.addWebHook.showLoading = true;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('webhooks/create', {
|
||||
webhook: { url: this.endPoint },
|
||||
});
|
||||
this.addWebHook.showLoading = false;
|
||||
|
||||
this.addWebHook.message = this.$t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE'
|
||||
);
|
||||
this.resetForm();
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
this.addWebHook.showLoading = false;
|
||||
this.addWebHook.message =
|
||||
error.response.data.message ||
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.addWebHook.showLoading = false;
|
||||
this.showAlert(this.addWebHook.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')"
|
||||
/>
|
||||
<webhook-form
|
||||
:value="value"
|
||||
:is-submitting="uiFlags.updatingItem"
|
||||
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.EDIT_SUBMIT')"
|
||||
@submit="onSubmit"
|
||||
@cancel="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import WebhookForm from './WebhookForm.vue';
|
||||
|
||||
export default {
|
||||
components: { WebhookForm },
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'webhooks/getUIFlags' }),
|
||||
},
|
||||
methods: {
|
||||
async onSubmit(webhook) {
|
||||
try {
|
||||
await this.$store.dispatch('webhooks/update', {
|
||||
webhook,
|
||||
id: this.id,
|
||||
});
|
||||
this.showAlert(
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
const alertMessage =
|
||||
error.response.data.message ||
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
||||
this.showAlert(alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -4,7 +4,7 @@
|
|||
color-scheme="success"
|
||||
class-names="button--fixed-right-top"
|
||||
icon="add-circle"
|
||||
@click="openAddPopup()"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
|
@ -37,42 +37,21 @@
|
|||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(webHookItem, index) in records" :key="webHookItem.id">
|
||||
<td class="webhook-link">
|
||||
{{ webHookItem.url }}
|
||||
</td>
|
||||
<td class="button-wrapper">
|
||||
<woot-button
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')
|
||||
"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="openEditPopup(webHookItem)"
|
||||
>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
||||
"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
@click="openDeletePopup(webHookItem, index)"
|
||||
>
|
||||
</woot-button>
|
||||
</td>
|
||||
</tr>
|
||||
<webhook-row
|
||||
v-for="(webHookItem, index) in records"
|
||||
:key="webHookItem.id"
|
||||
:index="index"
|
||||
:webhook="webHookItem"
|
||||
@edit="openEditPopup"
|
||||
@delete="openDeletePopup"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="small-4 columns">
|
||||
<span
|
||||
v-html="
|
||||
v-dompurify-html="
|
||||
useInstallationName(
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.SIDEBAR_TXT'),
|
||||
globalConfig.installationName
|
||||
|
@ -83,24 +62,27 @@
|
|||
</div>
|
||||
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<new-webhook :on-close="hideAddPopup" />
|
||||
<new-webhook v-if="showAddPopup" :on-close="hideAddPopup" />
|
||||
</woot-modal>
|
||||
|
||||
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
|
||||
<edit-webhook
|
||||
v-if="showEditPopup"
|
||||
:id="selectedWebHook.id"
|
||||
:url="selectedWebHook.url"
|
||||
:value="selectedWebHook"
|
||||
:on-close="hideEditPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')"
|
||||
:message="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')"
|
||||
:message="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE', {
|
||||
webhookURL: selectedWebHook.url,
|
||||
})
|
||||
"
|
||||
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
|
||||
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||
/>
|
||||
|
@ -112,11 +94,13 @@ import NewWebhook from './NewWebHook';
|
|||
import EditWebhook from './EditWebHook';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import WebhookRow from './WebhookRow';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewWebhook,
|
||||
EditWebhook,
|
||||
WebhookRow,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin],
|
||||
data() {
|
||||
|
@ -179,11 +163,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.webhook-link {
|
||||
word-break: break-word;
|
||||
}
|
||||
.button-wrapper button:nth-child(2) {
|
||||
margin-left: var(--space-normal);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.TITLE')"
|
||||
:header-content="
|
||||
useInstallationName(
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.DESC'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
/>
|
||||
<webhook-form
|
||||
:is-submitting="uiFlags.creatingItem"
|
||||
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.ADD_SUBMIT')"
|
||||
@submit="onSubmit"
|
||||
@cancel="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import WebhookForm from './WebhookForm.vue';
|
||||
|
||||
export default {
|
||||
components: { WebhookForm },
|
||||
mixins: [alertMixin, globalConfigMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
uiFlags: 'webhooks/getUIFlags',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async onSubmit(webhook) {
|
||||
try {
|
||||
await this.$store.dispatch('webhooks/create', { webhook });
|
||||
this.showAlert(
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.ADD.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
const message =
|
||||
error.response.data.message ||
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
|
||||
this.showAlert(message);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<form class="row" @submit.prevent="onSubmit">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.url.$error }">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.LABEL') }}
|
||||
<input
|
||||
v-model.trim="url"
|
||||
type="text"
|
||||
name="url"
|
||||
:placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.PLACEHOLDER')
|
||||
"
|
||||
@input="$v.url.$touch"
|
||||
/>
|
||||
<span v-if="$v.url.$error" class="message">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: $v.url.$error }" class="margin-bottom-small">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }}
|
||||
</label>
|
||||
<div v-for="event in supportedWebhookEvents" :key="event">
|
||||
<input
|
||||
:id="event"
|
||||
v-model="subscriptions"
|
||||
type="checkbox"
|
||||
:value="event"
|
||||
name="subscriptions"
|
||||
class="margin-right-small"
|
||||
/>
|
||||
<span class="fs-small">
|
||||
{{ `${getEventLabel(event)} (${event})` }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-button
|
||||
:disabled="$v.$invalid || isSubmitting"
|
||||
:is-loading="isSubmitting"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</woot-button>
|
||||
<woot-button class="button clear" @click.prevent="$emit('cancel')">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, url, minLength } from 'vuelidate/lib/validators';
|
||||
import webhookMixin from './webhookMixin';
|
||||
|
||||
const SUPPORTED_WEBHOOK_EVENTS = [
|
||||
'conversation_created',
|
||||
'conversation_status_changed',
|
||||
'conversation_updated',
|
||||
'message_created',
|
||||
'message_updated',
|
||||
'webwidget_triggered',
|
||||
];
|
||||
|
||||
export default {
|
||||
mixins: [webhookMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
submitLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
url: {
|
||||
required,
|
||||
minLength: minLength(7),
|
||||
url,
|
||||
},
|
||||
subscriptions: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
url: this.value.url || '',
|
||||
subscriptions: this.value.subscriptions || [],
|
||||
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$emit('submit', {
|
||||
url: this.url,
|
||||
subscriptions: this.subscriptions,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="webhook--link">{{ webhook.url }}</div>
|
||||
<span class="webhook--subscribed-events">
|
||||
<span class="webhook--subscribed-label">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.SUBSCRIBED_EVENTS') }}:
|
||||
</span>
|
||||
<show-more :text="subscribedEvents" :limit="60" />
|
||||
</span>
|
||||
</td>
|
||||
<td class="button-wrapper">
|
||||
<woot-button
|
||||
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="$emit('edit', webhook)"
|
||||
>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
size="tiny"
|
||||
icon="dismiss-circle"
|
||||
@click="$emit('delete', webhook, index)"
|
||||
>
|
||||
</woot-button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
import webhookMixin from './webhookMixin';
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore';
|
||||
|
||||
export default {
|
||||
components: { ShowMore },
|
||||
mixins: [webhookMixin],
|
||||
props: {
|
||||
webhook: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
subscribedEvents() {
|
||||
const { subscriptions } = this.webhook;
|
||||
return subscriptions.map(event => this.getEventLabel(event)).join(', ');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.webhook--link {
|
||||
color: var(--s-700);
|
||||
font-weight: var(--font-weight-medium);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.webhook--subscribed-events {
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.webhook--subscribed-label {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
max-width: var(--space-mega);
|
||||
min-width: auto;
|
||||
|
||||
button:nth-child(2) {
|
||||
margin-left: var(--space-normal);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,26 @@
|
|||
import { createWrapper } from '@vue/test-utils';
|
||||
import webhookMixin from '../webhookMixin';
|
||||
import Vue from 'vue';
|
||||
|
||||
describe('webhookMixin', () => {
|
||||
describe('#getEventLabel', () => {
|
||||
it('returns correct i18n translation:', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'WebhookComponent',
|
||||
mixins: [webhookMixin],
|
||||
methods: {
|
||||
$t(text) {
|
||||
return text;
|
||||
},
|
||||
},
|
||||
};
|
||||
const Constructor = Vue.extend(Component);
|
||||
const vm = new Constructor().$mount();
|
||||
const wrapper = createWrapper(vm);
|
||||
expect(wrapper.vm.getEventLabel('message_created')).toEqual(
|
||||
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.MESSAGE_CREATED`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
methods: {
|
||||
getEventLabel(event) {
|
||||
const eventName = event.toUpperCase();
|
||||
return this.$t(
|
||||
`INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.EVENTS.${eventName}`
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue