Merge branch 'develop' of https://github.com/chatwoot/chatwoot into ui/agent-dropdown
This commit is contained in:
commit
4f79c13977
272 changed files with 6878 additions and 2262 deletions
|
@ -116,6 +116,10 @@ SLACK_CLIENT_SECRET=
|
|||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
|
||||
ANDROID_BUNDLE_ID=com.chatwoot.app
|
||||
|
||||
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
|
||||
ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8
|
||||
|
||||
|
||||
### Smart App Banner
|
||||
|
@ -143,6 +147,11 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||
# maxmindb api key to use geoip2 service
|
||||
# IP_LOOKUP_API_KEY=
|
||||
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
## setting this value to true will disable the frontend dashboard endpoints
|
||||
# CW_API_ONLY_SERVER=false
|
||||
|
||||
## Development Only Config
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
|
|
|
@ -44,6 +44,9 @@ Metrics/BlockLength:
|
|||
- '**/routes.rb'
|
||||
- 'config/environments/*'
|
||||
- db/schema.rb
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- lib/woot_message_seeder.rb
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -59,6 +59,7 @@ gem 'barnes'
|
|||
|
||||
##--- gems for authentication & authorization ---##
|
||||
gem 'devise'
|
||||
gem 'devise-secure_password', '~> 2.0'
|
||||
gem 'devise_token_auth'
|
||||
# authorization
|
||||
gem 'jwt'
|
||||
|
@ -72,7 +73,7 @@ gem 'wisper', '2.0.0'
|
|||
|
||||
##--- gems for channels ---##
|
||||
# TODO: bump up gem to 2.0
|
||||
gem 'facebook-messenger', '1.5.0'
|
||||
gem 'facebook-messenger'
|
||||
gem 'telegram-bot-ruby'
|
||||
gem 'twilio-ruby', '~> 5.32.0'
|
||||
# twitty will handle subscription of twitter account events
|
||||
|
@ -132,8 +133,6 @@ group :test do
|
|||
end
|
||||
|
||||
group :development, :test do
|
||||
# locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved
|
||||
gem 'action-cable-testing'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
gem 'factory_bot_rails'
|
||||
|
|
25
Gemfile.lock
25
Gemfile.lock
|
@ -16,8 +16,6 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
action-cable-testing (0.6.1)
|
||||
actioncable (>= 5.0)
|
||||
actioncable (6.0.3.7)
|
||||
actionpack (= 6.0.3.7)
|
||||
nio4r (~> 2.0)
|
||||
|
@ -125,7 +123,7 @@ GEM
|
|||
barnes (0.0.8)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.15)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.4.8)
|
||||
msgpack (~> 1.0)
|
||||
|
@ -160,12 +158,15 @@ GEM
|
|||
declarative-option (0.1.0)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (4.7.2)
|
||||
devise (4.8.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-secure_password (2.0.1)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
railties (>= 5.0.0, < 7.0.0)
|
||||
devise_token_auth (1.1.4)
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
|
@ -188,7 +189,7 @@ GEM
|
|||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
execjs (2.7.0)
|
||||
facebook-messenger (1.5.0)
|
||||
facebook-messenger (2.0.1)
|
||||
httparty (~> 0.13, >= 0.13.7)
|
||||
rack (>= 1.4.5)
|
||||
factory_bot (6.1.0)
|
||||
|
@ -261,7 +262,7 @@ GEM
|
|||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
groupdate (5.1.0)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.37.1)
|
||||
google-protobuf (~> 3.15)
|
||||
|
@ -335,7 +336,7 @@ GEM
|
|||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2020.0512)
|
||||
mime-types-data (3.2021.0225)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.1.0)
|
||||
mini_portile2 (2.5.1)
|
||||
|
@ -350,7 +351,7 @@ GEM
|
|||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.7)
|
||||
nokogiri (1.11.4)
|
||||
nokogiri (1.11.6)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
|
@ -584,8 +585,8 @@ GEM
|
|||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.0.4)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
|
@ -613,7 +614,6 @@ PLATFORMS
|
|||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
action-cable-testing
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
|
@ -632,9 +632,10 @@ DEPENDENCIES
|
|||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
devise
|
||||
devise-secure_password (~> 2.0)
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
facebook-messenger (= 1.5.0)
|
||||
facebook-messenger
|
||||
factory_bot_rails
|
||||
faker
|
||||
fcm
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
require 'facebook/messenger'
|
||||
|
||||
class FacebookBot
|
||||
include Facebook::Messenger
|
||||
|
||||
Bot.on :message do |message|
|
||||
Facebook::Messenger::Bot.on :message do |message|
|
||||
Rails.logger.info "MESSAGE_RECIEVED #{message}"
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
end
|
||||
|
||||
Bot.on :delivery do |delivery|
|
||||
Facebook::Messenger::Bot.on :delivery do |delivery|
|
||||
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
|
||||
# delivery.sender # => { 'id' => '1008372609250235' }
|
||||
# delivery.recipient # => { 'id' => '2015573629214912' }
|
||||
|
@ -20,7 +18,7 @@ class FacebookBot
|
|||
Rails.logger.info "Human was online at #{delivery.at}"
|
||||
end
|
||||
|
||||
Bot.on :message_echo do |message|
|
||||
Facebook::Messenger::Bot.on :message_echo do |message|
|
||||
Rails.logger.info "MESSAGE_ECHO #{message}"
|
||||
response = ::Integrations::Facebook::MessageParser.new(message)
|
||||
::Integrations::Facebook::MessageCreator.new(response).perform
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!, :confirmed!, :user, :user_full_name, :user_password]
|
||||
pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password]
|
||||
|
||||
def perform
|
||||
if @user.nil?
|
||||
|
@ -61,11 +61,9 @@ class AccountBuilder
|
|||
end
|
||||
|
||||
def create_user
|
||||
password = user_password || SecureRandom.alphanumeric(12)
|
||||
|
||||
@user = User.new(email: @email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
password: user_password,
|
||||
password_confirmation: user_password,
|
||||
name: @user_full_name)
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
|
|
|
@ -13,6 +13,7 @@ class Messages::Facebook::MessageBuilder
|
|||
@outgoing_echo = outgoing_echo
|
||||
@sender_id = (@outgoing_echo ? @response.recipient_id : @response.sender_id)
|
||||
@message_type = (@outgoing_echo ? :outgoing : :incoming)
|
||||
@attachments = (@response.attachments || [])
|
||||
end
|
||||
|
||||
def perform
|
||||
|
@ -41,13 +42,19 @@ class Messages::Facebook::MessageBuilder
|
|||
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
(response.attachments || []).each do |attachment|
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
@attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
file_resource = LocalResource.new(file_url)
|
||||
attachment.file.attach(io: file_resource.file, filename: file_resource.filename, content_type: file_resource.encoding)
|
||||
|
|
|
@ -39,7 +39,17 @@ class Messages::MessageBuilder
|
|||
end
|
||||
|
||||
def sender
|
||||
message_type == 'outgoing' ? @user : @conversation.contact
|
||||
message_type == 'outgoing' ? (message_sender || @user) : @conversation.contact
|
||||
end
|
||||
|
||||
def external_created_at
|
||||
@params[:external_created_at].present? ? { external_created_at: @params[:external_created_at] } : {}
|
||||
end
|
||||
|
||||
def message_sender
|
||||
return if @params[:sender_type] != 'AgentBot'
|
||||
|
||||
AgentBot.where(account_id: [nil, @conversation.account.id]).find_by(id: @params[:sender_id])
|
||||
end
|
||||
|
||||
def message_params
|
||||
|
@ -54,6 +64,6 @@ class Messages::MessageBuilder
|
|||
items: @items,
|
||||
in_reply_to: @in_reply_to,
|
||||
echo_id: @params[:echo_id]
|
||||
}
|
||||
}.merge(external_created_at)
|
||||
end
|
||||
end
|
||||
|
|
5
app/controllers/android_app_controller.rb
Normal file
5
app/controllers/android_app_controller.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class AndroidAppController < ApplicationController
|
||||
def assetlinks
|
||||
render layout: false
|
||||
end
|
||||
end
|
|
@ -16,4 +16,8 @@ class Api::BaseController < ApplicationController
|
|||
|
||||
authorize(model)
|
||||
end
|
||||
|
||||
def check_admin_authorization?
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
|
35
app/controllers/api/v1/accounts/agent_bots_controller.rb
Normal file
35
app/controllers/api/v1/accounts/agent_bots_controller.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action :check_authorization
|
||||
before_action :agent_bot, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@agent_bots = AgentBot.where(account_id: [nil, Current.account.id])
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@agent_bot = Current.account.agent_bots.create!(permitted_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@agent_bot.update!(permitted_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent_bot.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def agent_bot
|
||||
@agent_bot = AgentBot.where(account_id: [nil, Current.account.id]).find(params[:id]) if params[:action] == 'show'
|
||||
@agent_bot ||= Current.account.agent_bots.find(params[:id])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :description, :outgoing_url)
|
||||
end
|
||||
end
|
|
@ -38,6 +38,8 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||
@user = User.find_by(email: new_agent_params[:email])
|
||||
end
|
||||
|
||||
# TODO: move this to a builder and combine the save account user method into a builder
|
||||
# ensure the account user association is also created in a single transaction
|
||||
def create_user
|
||||
return if @user
|
||||
|
||||
|
@ -58,9 +60,10 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def new_agent_params
|
||||
time = Time.now.to_i
|
||||
# intial string ensures the password requirements are met
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
.merge!(password: time, password_confirmation: time, inviter: current_user)
|
||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
||||
end
|
||||
|
||||
def agents
|
||||
|
|
|
@ -11,6 +11,7 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
|
|||
|
||||
def ensure_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
authorize @inbox, :show?
|
||||
end
|
||||
|
||||
def ensure_contact
|
||||
|
|
|
@ -8,9 +8,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
|
|||
private
|
||||
|
||||
def inbox_ids
|
||||
if Current.user.administrator?
|
||||
Current.account.inboxes.pluck(:id)
|
||||
elsif Current.user.agent?
|
||||
if Current.user.administrator? || Current.user.agent?
|
||||
Current.user.assigned_inboxes.pluck(:id)
|
||||
else
|
||||
[]
|
||||
|
|
|
@ -48,7 +48,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
def show; end
|
||||
|
||||
def contactable_inboxes
|
||||
@contactable_inboxes = Contacts::ContactableInboxesService.new(contact: @contact).get
|
||||
@all_contactable_inboxes = Contacts::ContactableInboxesService.new(contact: @contact).get
|
||||
@contactable_inboxes = @all_contactable_inboxes.select { |contactable_inbox| policy(contactable_inbox[:inbox]).show? }
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -5,5 +5,6 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base
|
|||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
|
||||
authorize @conversation.inbox, :show?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
include Events::Types
|
||||
|
||||
before_action :conversation, except: [:index]
|
||||
before_action :conversation, except: [:index, :meta, :search, :create]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
|
||||
def index
|
||||
|
@ -41,7 +41,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
end
|
||||
|
||||
def transcript
|
||||
ConversationReplyMailer.conversation_transcript(@conversation, params[:email])&.deliver_later if params[:email].present?
|
||||
render json: { error: 'email param missing' }, status: :unprocessable_entity and return if params[:email].blank?
|
||||
|
||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, params[:email])&.deliver_later
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
@ -77,34 +79,40 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:id])
|
||||
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
|
||||
authorize @conversation.inbox, :show?
|
||||
end
|
||||
|
||||
def contact_inbox
|
||||
@contact_inbox = build_contact_inbox
|
||||
|
||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||
authorize @contact_inbox.inbox, :show?
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
return if params[:contact_id].blank? || params[:inbox_id].blank?
|
||||
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
authorize inbox, :show?
|
||||
|
||||
ContactInboxBuilder.new(
|
||||
contact_id: params[:contact_id],
|
||||
inbox_id: params[:inbox_id],
|
||||
inbox_id: inbox.id,
|
||||
source_id: params[:source_id]
|
||||
).perform
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
{
|
||||
account_id: Current.account.id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes
|
||||
}
|
||||
}.merge(status)
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
class Api::V1::Accounts::FacebookIndicatorsController < Api::V1::Accounts::BaseController
|
||||
before_action :set_access_token
|
||||
around_action :handle_with_exception
|
||||
|
||||
def mark_seen
|
||||
fb_bot.deliver(payload('mark_seen'), access_token: @access_token)
|
||||
head :ok
|
||||
end
|
||||
|
||||
def typing_on
|
||||
fb_bot.deliver(payload('typing_on'), access_token: @access_token)
|
||||
head :ok
|
||||
end
|
||||
|
||||
def typing_off
|
||||
fb_bot.deliver(payload('typing_off'), access_token: @access_token)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fb_bot
|
||||
::Facebook::Messenger::Bot
|
||||
end
|
||||
|
||||
def handle_with_exception
|
||||
yield
|
||||
rescue Facebook::Messenger::Error => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
true
|
||||
end
|
||||
|
||||
def payload(action)
|
||||
{
|
||||
recipient: { id: contact.source_id },
|
||||
sender_action: action
|
||||
}
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= Current.account.inboxes.find(permitted_params[:inbox_id])
|
||||
end
|
||||
|
||||
def set_access_token
|
||||
@access_token = inbox.channel.page_access_token
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= inbox.contact_inboxes.find_by!(contact_id: permitted_params[:contact_id])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:inbox_id, :contact_id)
|
||||
end
|
||||
end
|
|
@ -3,15 +3,19 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
|||
before_action :current_agents_ids, only: [:create]
|
||||
|
||||
def create
|
||||
# update also done via same action
|
||||
update_agents_list
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
render_could_not_create_error('Could not add agents to inbox')
|
||||
authorize @inbox, :create?
|
||||
begin
|
||||
# update also done via same action
|
||||
update_agents_list
|
||||
head :ok
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug "Rescued: #{e.inspect}"
|
||||
render_could_not_create_error('Could not add agents to inbox')
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @inbox, :show?
|
||||
@agents = Current.account.users.where(id: @inbox.members.select(:user_id))
|
||||
end
|
||||
|
||||
|
|
|
@ -38,6 +38,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
update_channel_feature_flags
|
||||
end
|
||||
|
||||
def agent_bot
|
||||
@agent_bot = @inbox.agent_bot
|
||||
end
|
||||
|
||||
def set_agent_bot
|
||||
if @agent_bot
|
||||
agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox)
|
||||
|
@ -58,6 +62,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
|
||||
def fetch_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:id])
|
||||
authorize @inbox, :show?
|
||||
end
|
||||
|
||||
def fetch_agent_bot
|
||||
|
@ -83,12 +88,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, channel:
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time])
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||
params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||
:working_hours_enabled, :out_of_office_message, :timezone,
|
||||
channel: [
|
||||
:website_url,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_admin_authorization?
|
||||
before_action :fetch_apps, only: [:index]
|
||||
before_action :fetch_app, only: [:show]
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||
before_action :check_admin_authorization?
|
||||
before_action :fetch_hook, only: [:update, :destroy]
|
||||
|
||||
def create
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
account_name: account_params[:account_name],
|
||||
user_full_name: account_params[:user_full_name],
|
||||
email: account_params[:email],
|
||||
confirmed: confirmed?,
|
||||
user_password: account_params[:password],
|
||||
user: current_user
|
||||
).perform
|
||||
if @user
|
||||
|
@ -46,17 +46,13 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def confirmed?
|
||||
super_admin? && params[:confirmed]
|
||||
end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||
params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
class Api::V1::AgentBotsController < Api::BaseController
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
render json: AgentBot.all
|
||||
end
|
||||
end
|
|
@ -6,6 +6,12 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
end
|
||||
|
||||
def update
|
||||
if password_params[:password].present?
|
||||
render_could_not_create_error('Invalid current password') and return unless @user.valid_password?(password_params[:current_password])
|
||||
|
||||
@user.update!(password_params.except(:current_password))
|
||||
end
|
||||
|
||||
@user.update!(profile_params)
|
||||
end
|
||||
|
||||
|
@ -20,11 +26,17 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
:email,
|
||||
:name,
|
||||
:display_name,
|
||||
:password,
|
||||
:password_confirmation,
|
||||
:avatar,
|
||||
:availability,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:profile).permit(
|
||||
:current_password,
|
||||
:password,
|
||||
:password_confirmation
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
|
||||
def transcript
|
||||
if permitted_params[:email].present? && conversation.present?
|
||||
ConversationReplyMailer.conversation_transcript(
|
||||
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(
|
||||
conversation,
|
||||
permitted_params[:email]
|
||||
)&.deliver_later
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||
@message.update!(message_update_params[:message])
|
||||
end
|
||||
rescue StandardError => e
|
||||
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
|
||||
render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
|
||||
def account
|
||||
builder = V2::ReportBuilder.new(Current.account, account_report_params)
|
||||
data = builder.build
|
||||
|
@ -23,6 +25,10 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
|
||||
def account_summary_params
|
||||
{
|
||||
type: :account,
|
||||
|
|
|
@ -17,13 +17,8 @@ module AccessTokenAuthHelper
|
|||
Current.user = @resource if current_user.is_a?(User)
|
||||
end
|
||||
|
||||
def super_admin?
|
||||
@resource.present? && @resource.is_a?(SuperAdmin)
|
||||
end
|
||||
|
||||
def validate_bot_access_token!
|
||||
return if Current.user.is_a?(User)
|
||||
return if super_admin?
|
||||
return if agent_bot_accessible?
|
||||
|
||||
render_unauthorized('Access to this endpoint is not authorized for bots')
|
||||
|
|
|
@ -21,7 +21,9 @@ class DashboardController < ActionController::Base
|
|||
'PRIVACY_URL',
|
||||
'DISPLAY_MANIFEST',
|
||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
||||
'CHATWOOT_INBOX_TOKEN'
|
||||
'CHATWOOT_INBOX_TOKEN',
|
||||
'API_CHANNEL_NAME',
|
||||
'API_CHANNEL_THUMBNAIL'
|
||||
).merge(
|
||||
APP_VERSION: Chatwoot.config[:version]
|
||||
)
|
||||
|
|
|
@ -1,34 +1,29 @@
|
|||
class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
|
||||
include AuthHelper
|
||||
skip_before_action :require_no_authentication, raise: false
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
|
||||
def create
|
||||
@confirmable = User.find_by(confirmation_token: params[:confirmation_token])
|
||||
render_confirmation_success and return if @confirmable&.confirm
|
||||
|
||||
if confirm
|
||||
render_confirmation_success
|
||||
else
|
||||
render_confirmation_error
|
||||
end
|
||||
render_confirmation_error
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def confirm
|
||||
@confirmable&.confirm || (@confirmable&.confirmed_at && @confirmable&.reset_password_token)
|
||||
end
|
||||
private
|
||||
|
||||
def render_confirmation_success
|
||||
render json: { "message": 'Success', "redirect_url": create_reset_token_link(@confirmable) }, status: :ok
|
||||
send_auth_headers(@confirmable)
|
||||
render partial: 'devise/auth.json', locals: { resource: @confirmable }
|
||||
end
|
||||
|
||||
def render_confirmation_error
|
||||
if @confirmable.blank?
|
||||
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
|
||||
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
|
||||
elsif @confirmable.confirmed_at
|
||||
render json: { "message": 'Already confirmed', "redirect_url": '/' }, status: 422
|
||||
render json: { message: 'Already confirmed', redirect_url: '/' }, status: :unprocessable_entity
|
||||
else
|
||||
render json: { "message": 'Failure', "redirect_url": '/' }, status: 422
|
||||
render json: { message: 'Failure', redirect_url: '/' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||
send_auth_headers(@recoverable)
|
||||
render partial: 'devise/auth.json', locals: { resource: @recoverable }
|
||||
else
|
||||
render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422
|
||||
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -27,7 +27,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||
end
|
||||
end
|
||||
|
||||
protected
|
||||
private
|
||||
|
||||
def reset_password_and_confirmation(recoverable)
|
||||
recoverable.confirm unless recoverable.confirmed? # confirm if user resets password without confirming anytime before
|
||||
|
@ -40,7 +40,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||
|
||||
def build_response(message, status)
|
||||
render json: {
|
||||
"message": message
|
||||
message: message
|
||||
}, status: status
|
||||
end
|
||||
end
|
||||
|
|
35
app/controllers/platform/api/v1/agent_bots_controller.rb
Normal file
35
app/controllers/platform/api/v1/agent_bots_controller.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
class Platform::Api::V1::AgentBotsController < PlatformController
|
||||
before_action :set_resource, except: [:index, :create]
|
||||
before_action :validate_platform_app_permissible, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').all
|
||||
end
|
||||
|
||||
def create
|
||||
@resource = AgentBot.new(agent_bot_params)
|
||||
@resource.save!
|
||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@resource.update!(agent_bot_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@resource.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_resource
|
||||
@resource = AgentBot.find(params[:id])
|
||||
end
|
||||
|
||||
def agent_bot_params
|
||||
params.permit(:name, :description, :account_id, :outgoing_url)
|
||||
end
|
||||
end
|
|
@ -15,8 +15,7 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||
description: Field::String,
|
||||
outgoing_url: Field::String,
|
||||
created_at: Field::DateTime,
|
||||
updated_at: Field::DateTime,
|
||||
hide_input_for_bot_conversations: Field::Boolean
|
||||
updated_at: Field::DateTime
|
||||
}.freeze
|
||||
|
||||
# COLLECTION_ATTRIBUTES
|
||||
|
@ -39,7 +38,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||
name
|
||||
description
|
||||
outgoing_url
|
||||
hide_input_for_bot_conversations
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
@ -49,7 +47,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
|||
name
|
||||
description
|
||||
outgoing_url
|
||||
hide_input_for_bot_conversations
|
||||
].freeze
|
||||
|
||||
# COLLECTION_FILTERS
|
||||
|
|
|
@ -11,7 +11,6 @@ class SuperAdminDashboard < Administrate::BaseDashboard
|
|||
id: Field::Number,
|
||||
email: Field::String,
|
||||
password: Field::Password,
|
||||
access_token: Field::HasOne,
|
||||
remember_created_at: Field::DateTime,
|
||||
sign_in_count: Field::Number,
|
||||
current_sign_in_at: Field::DateTime,
|
||||
|
@ -30,7 +29,6 @@ class SuperAdminDashboard < Administrate::BaseDashboard
|
|||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
email
|
||||
access_token
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
|
|
|
@ -48,15 +48,11 @@ class ConversationFinder
|
|||
private
|
||||
|
||||
def set_inboxes
|
||||
if params[:inbox_id]
|
||||
@inbox_ids = current_account.inboxes.where(id: params[:inbox_id])
|
||||
else
|
||||
if @current_user.administrator?
|
||||
@inbox_ids = current_account.inboxes.pluck(:id)
|
||||
elsif @current_user.agent?
|
||||
@inbox_ids = @current_user.assigned_inboxes.pluck(:id)
|
||||
end
|
||||
end
|
||||
@inbox_ids = if params[:inbox_id]
|
||||
current_account.inboxes.where(id: params[:inbox_id])
|
||||
else
|
||||
@current_user.assigned_inboxes.pluck(:id)
|
||||
end
|
||||
end
|
||||
|
||||
def set_assignee_type
|
||||
|
|
|
@ -29,6 +29,7 @@ export default {
|
|||
account_name: creds.accountName.trim(),
|
||||
user_full_name: creds.fullName.trim(),
|
||||
email: creds.email,
|
||||
password: creds.password,
|
||||
})
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
|
@ -95,8 +96,18 @@ export default {
|
|||
},
|
||||
|
||||
verifyPasswordToken({ confirmationToken }) {
|
||||
return axios.post('auth/confirmation', {
|
||||
confirmation_token: confirmationToken,
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('auth/confirmation', {
|
||||
confirmation_token: confirmationToken,
|
||||
})
|
||||
.then(response => {
|
||||
setAuthCredentials(response);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error.response);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -16,6 +16,14 @@ class IntegrationsAPI extends ApiClient {
|
|||
delete(integrationId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
|
||||
}
|
||||
|
||||
createHook(hookData) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/hooks`, hookData);
|
||||
}
|
||||
|
||||
deleteHook(hookId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new IntegrationsAPI();
|
||||
|
|
|
@ -5,6 +5,7 @@ function apiSpecHelper() {
|
|||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
window.axios = this.axiosMock;
|
||||
});
|
||||
|
|
55
app/javascript/dashboard/api/specs/integrations.spec.js
Normal file
55
app/javascript/dashboard/api/specs/integrations.spec.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import integrationAPI from '../integrations';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#integrationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(integrationAPI).toBeInstanceOf(ApiClient);
|
||||
expect(integrationAPI).toHaveProperty('get');
|
||||
expect(integrationAPI).toHaveProperty('show');
|
||||
expect(integrationAPI).toHaveProperty('create');
|
||||
expect(integrationAPI).toHaveProperty('update');
|
||||
expect(integrationAPI).toHaveProperty('delete');
|
||||
expect(integrationAPI).toHaveProperty('connectSlack');
|
||||
expect(integrationAPI).toHaveProperty('createHook');
|
||||
expect(integrationAPI).toHaveProperty('deleteHook');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#connectSlack', () => {
|
||||
const code = 'SDNFJNSDFNDSJN';
|
||||
integrationAPI.connectSlack(code);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/slack',
|
||||
{
|
||||
code,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#delete', () => {
|
||||
integrationAPI.delete(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/2'
|
||||
);
|
||||
});
|
||||
|
||||
it('#createHook', () => {
|
||||
const hookData = {
|
||||
app_id: 'fullcontact',
|
||||
settings: { api_key: 'SDFSDGSVE' },
|
||||
};
|
||||
integrationAPI.createHook(hookData);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks',
|
||||
hookData
|
||||
);
|
||||
});
|
||||
|
||||
it('#deleteHook', () => {
|
||||
integrationAPI.deleteHook(2);
|
||||
expect(context.axiosMock.delete).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/hooks/2'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
18
app/javascript/dashboard/assets/scss/_formulate.scss
Normal file
18
app/javascript/dashboard/assets/scss/_formulate.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.formulate-input {
|
||||
.formulate-input-errors {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.formulate-input-error {
|
||||
color: var(--r-400);
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: $font-weight-normal;
|
||||
margin-bottom: $space-one;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -11,13 +11,13 @@
|
|||
@import 'mixins';
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
||||
@include foundation-everything($flex: true);
|
||||
|
||||
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
|
@ -46,5 +46,4 @@
|
|||
|
||||
@import 'plugins/multiselect';
|
||||
@import 'plugins/dropdown';
|
||||
@import '@chatwoot/prosemirror-schema/src/woot-editor.css';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
|
|
@ -31,8 +31,9 @@
|
|||
}
|
||||
|
||||
img {
|
||||
width: 50%;
|
||||
@include margin($space-normal auto);
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.channel__title{
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
.settings {
|
||||
overflow: auto;
|
||||
|
||||
.page-top-bar {
|
||||
@include padding($space-normal $space-two $zero);
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation header - Light BG
|
||||
|
@ -27,7 +23,6 @@
|
|||
@include flex-align($x: center, $y: middle);
|
||||
@include margin($zero);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.wizard-box {
|
||||
|
|
|
@ -1,31 +1,24 @@
|
|||
$default-button-height: 4.0rem;
|
||||
|
||||
.button {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
height: 4.0rem;
|
||||
height: $default-button-height;
|
||||
margin-bottom: 0;
|
||||
|
||||
&.button--emoji {
|
||||
align-items: center;
|
||||
background: var(--b-50);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--border-radius-large);
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
height: var(--space-large);
|
||||
justify-content: center;
|
||||
padding: var(--space-micro);
|
||||
text-align: center;
|
||||
width: var(--space-large);
|
||||
|
||||
&:hover {
|
||||
background: var(--b-200);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
padding: 0 var(--space-small);
|
||||
}
|
||||
|
||||
.icon--emoji+.button__content {
|
||||
padding-left: var(--space-small);
|
||||
}
|
||||
|
||||
.icon--font+.button__content {
|
||||
padding-left: var(--space-small);
|
||||
}
|
||||
|
||||
// @TODDO - Remove after moving all buttons to woot-button
|
||||
.icon+.button__content {
|
||||
padding-left: var(--space-small);
|
||||
}
|
||||
|
@ -48,23 +41,23 @@
|
|||
|
||||
// Smooth style
|
||||
&.smooth {
|
||||
@include button-style(var(--w-100), var(--w-50), var(--w-700));
|
||||
@include button-style(var(--w-50), var(--w-100), var(--w-700));
|
||||
|
||||
|
||||
&.secondary {
|
||||
@include button-style(var(--s-100), var(--s-50), var(--s-700));
|
||||
@include button-style(var(--s-50), var(--s-100), var(--s-700));
|
||||
}
|
||||
|
||||
&.success {
|
||||
@include button-style(var(--g-100), var(--g-50), var(--g-700));
|
||||
@include button-style(var(--g-50), var(--g-100), var(--g-700));
|
||||
}
|
||||
|
||||
&.alert {
|
||||
@include button-style(var(--r-100), var(--r-50), var(--r-700));
|
||||
@include button-style(var(--r-50), var(--r-100), var(--r-700));
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include button-style(var(--y-200), var(--y-100), var(--y-900));
|
||||
@include button-style(var(--y-100), var(--y-200), var(--y-900));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,6 +74,25 @@
|
|||
height: var(--space-larger);
|
||||
}
|
||||
|
||||
&.button--only-icon {
|
||||
justify-content: center;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
width: $default-button-height;
|
||||
|
||||
&.tiny {
|
||||
width: var(--space-medium);
|
||||
}
|
||||
|
||||
&.small {
|
||||
width: var(--space-large);
|
||||
}
|
||||
|
||||
&.large {
|
||||
width: var(--space-larger);
|
||||
}
|
||||
}
|
||||
|
||||
&.link {
|
||||
height: auto;
|
||||
margin: 0;
|
||||
|
|
|
@ -52,25 +52,4 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-uploads>label {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover .button--emoji {
|
||||
background: var(--b-200);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-box .button--emoji.button--upload {
|
||||
padding: 0;
|
||||
|
||||
.file-uploads {
|
||||
height: 100%;
|
||||
line-height: var(--space-large);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
padding: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
class-names="resolve"
|
||||
color-scheme="success"
|
||||
icon="ion-checkmark"
|
||||
emoji="✅"
|
||||
:is-loading="isLoading"
|
||||
@click="() => toggleStatus(STATUS_TYPE.RESOLVED)"
|
||||
>
|
||||
|
@ -16,6 +17,7 @@
|
|||
class-names="resolve"
|
||||
color-scheme="warning"
|
||||
icon="ion-refresh"
|
||||
emoji="👀"
|
||||
:is-loading="isLoading"
|
||||
@click="() => toggleStatus(STATUS_TYPE.OPEN)"
|
||||
>
|
||||
|
@ -36,9 +38,9 @@
|
|||
:color-scheme="buttonClass"
|
||||
:disabled="isLoading"
|
||||
icon="ion-arrow-down-b"
|
||||
emoji="🔽"
|
||||
@click="openDropdown"
|
||||
>
|
||||
</woot-button>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
|
|
|
@ -6,6 +6,7 @@ import Button from './ui/WootButton';
|
|||
import Code from './Code';
|
||||
import ColorPicker from './widgets/ColorPicker';
|
||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import Input from './widgets/forms/Input.vue';
|
||||
|
@ -42,6 +43,7 @@ const WootUIKit = {
|
|||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
ConfirmDeleteModal,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
|
|
@ -264,10 +264,6 @@ export default {
|
|||
.modal-container {
|
||||
width: 40rem;
|
||||
}
|
||||
|
||||
.page-top-bar {
|
||||
padding-bottom: $space-two;
|
||||
}
|
||||
}
|
||||
|
||||
.account-selector {
|
||||
|
|
|
@ -52,7 +52,11 @@ export default {
|
|||
},
|
||||
labelStyle() {
|
||||
if (this.bgColor) {
|
||||
return { background: this.bgColor, color: this.textColor };
|
||||
return {
|
||||
background: this.bgColor,
|
||||
color: this.textColor,
|
||||
border: `1px solid ${this.bgColor}`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
|
|
@ -6,16 +6,22 @@
|
|||
@click="handleClick"
|
||||
>
|
||||
<spinner v-if="isLoading" size="small" />
|
||||
<i v-else-if="icon" class="icon" :class="icon"></i>
|
||||
<emoji-or-icon
|
||||
v-else-if="icon || emoji"
|
||||
class="icon"
|
||||
:emoji="emoji"
|
||||
:icon="icon"
|
||||
/>
|
||||
<span v-if="$slots.default" class="button__content"><slot></slot></span>
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||
|
||||
export default {
|
||||
name: 'WootButton',
|
||||
components: { Spinner },
|
||||
components: { EmojiOrIcon, Spinner },
|
||||
props: {
|
||||
variant: {
|
||||
type: String,
|
||||
|
@ -29,12 +35,16 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
colorScheme: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
classNames: {
|
||||
type: String,
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
isDisabled: {
|
||||
|
@ -57,9 +67,15 @@ export default {
|
|||
}
|
||||
return this.variant;
|
||||
},
|
||||
hasOnlyIconClasses() {
|
||||
const hasEmojiOrIcon = this.emoji || this.icon;
|
||||
if (!this.$slots.default && hasEmojiOrIcon) return 'button--only-icon';
|
||||
return '';
|
||||
},
|
||||
buttonClasses() {
|
||||
return [
|
||||
this.variantClasses,
|
||||
this.hasOnlyIconClasses,
|
||||
this.size,
|
||||
this.colorScheme,
|
||||
this.classNames,
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
<template>
|
||||
<button :type="type" class="button nice" :class="variant" @click="onClick">
|
||||
<i
|
||||
v-if="!isLoading && icon"
|
||||
class="icon"
|
||||
:class="buttonIconClass + ' ' + icon"
|
||||
/>
|
||||
<spinner v-if="isLoading" />
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonIconClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
font-size: var(--font-size-large) !important;
|
||||
}
|
||||
</style>
|
|
@ -17,9 +17,13 @@
|
|||
src="~dashboard/assets/images/channels/telegram.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'api'"
|
||||
v-if="channel.key === 'api' && !channel.thumbnail"
|
||||
src="~dashboard/assets/images/channels/api.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'api' && channel.thumbnail"
|
||||
:src="channel.thumbnail"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'email'"
|
||||
src="~dashboard/assets/images/channels/email.png"
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
<template>
|
||||
<div class="inbox-item" >
|
||||
<img src="~dashboard/assets/images/no_page_image.png" alt="No Page Image"/>
|
||||
<div class="inbox-item">
|
||||
<img src="~dashboard/assets/images/no_page_image.png" alt="No Page Image" />
|
||||
<div class="item--details columns">
|
||||
<h4 class="item--name">{{ inbox.label }}</h4>
|
||||
<p class="item--sub">Facebook</p>
|
||||
<h4 class="item--name">
|
||||
{{ inbox.label }}
|
||||
</h4>
|
||||
<p class="item--sub">
|
||||
Facebook
|
||||
</p>
|
||||
</div>
|
||||
<!-- <span class="ion-chevron-right arrow"></span> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
/* global bus */
|
||||
// import WootSwitch from '../ui/Switch';
|
||||
|
||||
export default {
|
||||
props: ['inbox'],
|
||||
created() {
|
||||
},
|
||||
created() {},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import SettingIntroBanner from './SettingIntroBanner';
|
||||
|
||||
export default {
|
||||
title: 'Components/Settings/Banner',
|
||||
component: SettingIntroBanner,
|
||||
argTypes: {
|
||||
headerTitle: {
|
||||
defaultValue: 'Acme Support',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
headerContent: {
|
||||
defaultValue:
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { SettingIntroBanner },
|
||||
template: '<setting-intro-banner v-bind="$props" ></setting-intro-banner>',
|
||||
});
|
||||
|
||||
export const Banner = Template.bind({});
|
||||
Banner.args = {};
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="column page-top-banner">
|
||||
<h2 class="page-sub-title">
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p v-if="headerContent" class="small-12 column">
|
||||
{{ headerContent }}
|
||||
</p>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.page-top-banner {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-background-light);
|
||||
padding: var(--space-normal) var(--space-large) 0;
|
||||
}
|
||||
</style>
|
|
@ -5,30 +5,40 @@
|
|||
:search-key="mentionSearchKey"
|
||||
@click="insertMentionNode"
|
||||
/>
|
||||
<canned-response
|
||||
v-if="showCannedMenu"
|
||||
:search-key="cannedSearchTerm"
|
||||
@click="insertCannedResponse"
|
||||
/>
|
||||
<div ref="editor"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||
|
||||
import {
|
||||
addMentionsToMarkdownSerializer,
|
||||
addMentionsToMarkdownParser,
|
||||
schemaWithMentions,
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/schema';
|
||||
|
||||
import {
|
||||
suggestionsPlugin,
|
||||
triggerCharacters,
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||
import TagAgents from '../conversation/TagAgents.vue';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { defaultMarkdownParser } from 'prosemirror-markdown';
|
||||
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
|
||||
|
||||
import TagAgents from '../conversation/TagAgents';
|
||||
import CannedResponse from '../conversation/CannedResponse';
|
||||
|
||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||
|
||||
import '@chatwoot/prosemirror-schema/src/woot-editor.css';
|
||||
|
||||
const createState = (content, placeholder, plugins = []) => {
|
||||
return EditorState.create({
|
||||
doc: addMentionsToMarkdownParser(defaultMarkdownParser).parse(content),
|
||||
|
@ -42,7 +52,7 @@ const createState = (content, placeholder, plugins = []) => {
|
|||
|
||||
export default {
|
||||
name: 'WootMessageEditor',
|
||||
components: { TagAgents },
|
||||
components: { TagAgents, CannedResponse },
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
|
@ -52,7 +62,9 @@ export default {
|
|||
return {
|
||||
lastValue: null,
|
||||
showUserMentions: false,
|
||||
showCannedMenu: false,
|
||||
mentionSearchKey: '',
|
||||
cannedSearchTerm: '',
|
||||
editorView: null,
|
||||
range: null,
|
||||
};
|
||||
|
@ -85,6 +97,35 @@ export default {
|
|||
return event.keyCode === 13 && this.showUserMentions;
|
||||
},
|
||||
}),
|
||||
suggestionsPlugin({
|
||||
matcher: triggerCharacters('/'),
|
||||
suggestionClass: '',
|
||||
onEnter: args => {
|
||||
if (this.isPrivate) {
|
||||
return false;
|
||||
}
|
||||
this.showCannedMenu = true;
|
||||
this.range = args.range;
|
||||
this.editorView = args.view;
|
||||
return false;
|
||||
},
|
||||
onChange: args => {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
|
||||
this.cannedSearchTerm = args.text.replace('/', '');
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
this.cannedSearchTerm = '';
|
||||
this.showCannedMenu = false;
|
||||
this.editorView = null;
|
||||
return false;
|
||||
},
|
||||
onKeyDown: ({ event }) => {
|
||||
return event.keyCode === 13 && this.showCannedMenu;
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
@ -92,9 +133,14 @@ export default {
|
|||
showUserMentions(updatedValue) {
|
||||
this.$emit('toggle-user-mention', this.isPrivate && updatedValue);
|
||||
},
|
||||
value(newValue) {
|
||||
showCannedMenu(updatedValue) {
|
||||
this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
|
||||
},
|
||||
value(newValue = '') {
|
||||
if (newValue !== this.lastValue) {
|
||||
this.state = createState(newValue, this.placeholder, this.plugins);
|
||||
const { tr } = this.state;
|
||||
tr.insertText(newValue, 0, tr.doc.content.size);
|
||||
this.state = this.view.state.apply(tr);
|
||||
this.view.updateState(this.state);
|
||||
}
|
||||
},
|
||||
|
@ -140,6 +186,21 @@ export default {
|
|||
this.state = this.view.state.apply(tr);
|
||||
return this.emitOnChange();
|
||||
},
|
||||
|
||||
insertCannedResponse(cannedItem) {
|
||||
if (!this.view) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tr = this.view.state.tr.insertText(
|
||||
cannedItem,
|
||||
this.range.from,
|
||||
this.range.to
|
||||
);
|
||||
this.state = this.view.state.apply(tr);
|
||||
return this.emitOnChange();
|
||||
},
|
||||
|
||||
emitOnChange() {
|
||||
this.view.updateState(this.state);
|
||||
this.lastValue = addMentionsToMarkdownSerializer(
|
||||
|
@ -205,10 +266,9 @@ export default {
|
|||
.is-private {
|
||||
.prosemirror-mention-node {
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--s-300);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: 1px 4px;
|
||||
color: var(--white);
|
||||
background: var(--s-50);
|
||||
color: var(--s-900);
|
||||
padding: 0 var(--space-smaller);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,34 +1,42 @@
|
|||
<template>
|
||||
<div class="bottom-box" :class="wrapClass">
|
||||
<div class="left-wrap">
|
||||
<button
|
||||
class="button clear button--emoji"
|
||||
<woot-button
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||
icon="ion-happy-outline"
|
||||
emoji="😊"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleEmojiPicker"
|
||||
/>
|
||||
|
||||
<file-upload
|
||||
:size="4096 * 4096"
|
||||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<emoji-or-icon icon="ion-happy-outline" emoji="😊" />
|
||||
</button>
|
||||
<button
|
||||
v-if="showAttachButton"
|
||||
class="button clear button--emoji button--upload"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
>
|
||||
<file-upload
|
||||
:size="4096 * 4096"
|
||||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<emoji-or-icon icon="ion-android-attach" emoji="📎" />
|
||||
</file-upload>
|
||||
</button>
|
||||
<button
|
||||
<woot-button
|
||||
v-if="showAttachButton"
|
||||
class-names="button--upload"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||
icon="ion-android-attach"
|
||||
emoji="📎"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
/>
|
||||
</file-upload>
|
||||
<woot-button
|
||||
v-if="enableRichEditor && !isOnPrivateNote"
|
||||
class="button clear button--emoji"
|
||||
icon="ion-quote"
|
||||
emoji="🖊️"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
@click="toggleFormatMode"
|
||||
>
|
||||
<emoji-or-icon icon="ion-quote" emoji="🖊️" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<div v-if="isFormatMode" class="enter-to-send--checkbox">
|
||||
|
@ -42,25 +50,25 @@
|
|||
{{ $t('CONVERSATION.REPLYBOX.ENTER_TO_SEND') }}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
class="button nice primary button--send"
|
||||
:class="buttonClass"
|
||||
<woot-button
|
||||
size="small"
|
||||
:class-names="buttonClass"
|
||||
:is-disabled="isSendDisabled"
|
||||
@click="onSend"
|
||||
>
|
||||
{{ sendButtonText }}
|
||||
</button>
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
||||
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
components: { EmojiOrIcon, FileUpload },
|
||||
components: { FileUpload },
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
|
@ -126,8 +134,7 @@ export default {
|
|||
},
|
||||
buttonClass() {
|
||||
return {
|
||||
'button--note': this.isNote,
|
||||
'button--disabled': this.isSendDisabled,
|
||||
warning: this.isNote,
|
||||
};
|
||||
},
|
||||
showAttachButton() {
|
||||
|
@ -146,9 +153,6 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
@import '~widget/assets/scss/mixins.scss';
|
||||
|
||||
.bottom-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -159,39 +163,8 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
&.button--emoji {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: white;
|
||||
}
|
||||
|
||||
&.button--note {
|
||||
background: var(--y-800);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: var(--y-700);
|
||||
}
|
||||
}
|
||||
|
||||
&.button--disabled {
|
||||
background: var(--b-100);
|
||||
color: var(--b-400);
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: var(--b-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-box.is-note-mode {
|
||||
.button--emoji {
|
||||
background: white;
|
||||
}
|
||||
.left-wrap .button {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.left-wrap {
|
||||
|
@ -199,15 +172,6 @@ export default {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.button--reply {
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.icon--font {
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
display: flex;
|
||||
|
||||
|
@ -225,4 +189,13 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .file-uploads {
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover .button {
|
||||
background: var(--s-100);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -152,7 +152,10 @@ export default {
|
|||
return !messageType ? 'left' : 'right';
|
||||
},
|
||||
readableTime() {
|
||||
return this.messageStamp(this.data.created_at, 'LLL d, h:mm a');
|
||||
return this.messageStamp(
|
||||
this.contentAttributes.external_created_at || this.data.created_at,
|
||||
'LLL d, h:mm a'
|
||||
);
|
||||
},
|
||||
isBubble() {
|
||||
return [0, 1, 3].includes(this.data.message_type);
|
||||
|
@ -197,11 +200,16 @@ export default {
|
|||
'is-private': this.data.private,
|
||||
'is-image': this.hasImageAttachment,
|
||||
'is-text': this.hasText,
|
||||
'is-from-bot': this.isSentByBot,
|
||||
};
|
||||
},
|
||||
isPending() {
|
||||
return this.data.status === MESSAGE_STATUS.PROGRESS;
|
||||
},
|
||||
isSentByBot() {
|
||||
if (this.isPending) return false;
|
||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -250,6 +258,13 @@ export default {
|
|||
color: var(--color-body);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.is-from-bot {
|
||||
background: var(--v-400);
|
||||
.message-text--metadata .time {
|
||||
color: var(--v-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-pending {
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@toggle-user-mention="toggleUserMention"
|
||||
@toggle-canned-menu="toggleCannedMenu"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasAttachments" class="attachment-preview-box">
|
||||
|
@ -249,7 +250,8 @@ export default {
|
|||
}
|
||||
},
|
||||
message(updatedMessage) {
|
||||
this.hasSlashCommand = updatedMessage[0] === '/';
|
||||
this.hasSlashCommand =
|
||||
updatedMessage[0] === '/' && !this.showRichContentEditor;
|
||||
const hasNextWord = updatedMessage.includes(' ');
|
||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
|
@ -271,6 +273,9 @@ export default {
|
|||
toggleUserMention(currentMentionState) {
|
||||
this.hasUserMention = currentMentionState;
|
||||
},
|
||||
toggleCannedMenu(value) {
|
||||
this.showCannedMenu = value;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (isEscape(e)) {
|
||||
this.hideEmojiPicker();
|
||||
|
@ -279,7 +284,8 @@ export default {
|
|||
const hasSendOnEnterEnabled =
|
||||
(this.showRichContentEditor &&
|
||||
this.enterToSendEnabled &&
|
||||
!this.hasUserMention) ||
|
||||
!this.hasUserMention &&
|
||||
!this.showCannedMenu) ||
|
||||
!this.showRichContentEditor;
|
||||
const shouldSendMessage =
|
||||
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="closeModal">
|
||||
<woot-modal-header :header-title="title" :header-content="message" />
|
||||
<form @submit.prevent="onConfirm">
|
||||
<woot-input
|
||||
v-model="value"
|
||||
type="text"
|
||||
:class="{ error: $v.value.$error }"
|
||||
:placeholder="confirmPlaceHolderText"
|
||||
@blur="$v.value.$touch"
|
||||
/>
|
||||
<div class="button-wrapper">
|
||||
<woot-button color-scheme="alert" :is-disabled="$v.value.$invalid">
|
||||
{{ confirmText }}
|
||||
</woot-button>
|
||||
<woot-button class="clear" @click.prevent="closeModal">
|
||||
{{ rejectText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import Modal from '../../Modal';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rejectText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
confirmValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
confirmPlaceHolderText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
value: {
|
||||
required,
|
||||
isEqual(value) {
|
||||
return value === this.confirmValue;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.value = '';
|
||||
this.$emit('on-close');
|
||||
},
|
||||
onConfirm() {
|
||||
this.$emit('on-confirm');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,8 +1,4 @@
|
|||
export default {
|
||||
APP_BASE_URL: '/',
|
||||
get apiURL() {
|
||||
return `${this.APP_BASE_URL}/`;
|
||||
},
|
||||
GRAVATAR_URL: 'https://www.gravatar.com/avatar/',
|
||||
ASSIGNEE_TYPE: {
|
||||
ME: 'me',
|
||||
|
@ -15,3 +11,4 @@ export default {
|
|||
BOT: 'bot',
|
||||
},
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
/* eslint no-console: 0 */
|
||||
import constants from '../constants';
|
||||
import Auth from '../api/auth';
|
||||
|
||||
const parseErrorCode = error => {
|
||||
return Promise.reject(error);
|
||||
};
|
||||
const parseErrorCode = error => Promise.reject(error);
|
||||
|
||||
export default axios => {
|
||||
const wootApi = axios.create();
|
||||
wootApi.defaults.baseURL = constants.apiURL;
|
||||
const { apiHost = '' } = window.chatwootConfig || {};
|
||||
const wootApi = axios.create({ baseURL: `${apiHost}/` });
|
||||
// Add Auth Headers to requests if logged in
|
||||
if (Auth.isLoggedIn()) {
|
||||
const {
|
||||
|
|
|
@ -4,7 +4,8 @@ import { newMessageNotification } from 'shared/helpers/AudioNotificationHelper';
|
|||
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
super(app, pubsubToken);
|
||||
const { websocketURL = '' } = window.chatwootConfig || {};
|
||||
super(app, pubsubToken, websocketURL);
|
||||
this.CancelTyping = [];
|
||||
this.events = {
|
||||
'message.created': this.onMessageCreated,
|
||||
|
|
|
@ -13,6 +13,18 @@ export default () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const isEmptyObject = obj =>
|
||||
Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
|
||||
export const isJSONValid = value => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getTypingUsersText = (users = []) => {
|
||||
const count = users.length;
|
||||
if (count === 1) {
|
||||
|
|
|
@ -74,6 +74,9 @@ export const getSidebarItems = accountId => ({
|
|||
'settings_integrations',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'settings_applications',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications_integration',
|
||||
'general_settings',
|
||||
'general_settings_index',
|
||||
'settings_teams_list',
|
||||
|
@ -136,6 +139,13 @@ export const getSidebarItems = accountId => ({
|
|||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
},
|
||||
settings_applications: {
|
||||
icon: 'ion-asterisk',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
},
|
||||
general_settings_index: {
|
||||
icon: 'ion-gear-a',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
|
|
|
@ -155,6 +155,16 @@
|
|||
"VIEW_DETAILS": "View details"
|
||||
}
|
||||
},
|
||||
"REMINDER": {
|
||||
"ADD_BUTTON": {
|
||||
"BUTTON": "Add",
|
||||
"TITLE": "Shift + Enter to create a task"
|
||||
},
|
||||
"FOOTER": {
|
||||
"DUE_DATE": "Due date",
|
||||
"LABEL_TITLE": "Set type"
|
||||
}
|
||||
},
|
||||
"NOTES": {
|
||||
"HEADER": {
|
||||
"TITLE": "Notes"
|
||||
|
@ -168,6 +178,16 @@
|
|||
"BUTTON": "View all notes"
|
||||
}
|
||||
},
|
||||
"EVENTS": {
|
||||
"HEADER": {
|
||||
"TITLE": "Activities"
|
||||
},
|
||||
"BUTTON": {
|
||||
"PILL_BUTTON_NOTES": "notes",
|
||||
"PILL_BUTTON_EVENTS": "events",
|
||||
"PILL_BUTTON_CONVO": "conversations"
|
||||
}
|
||||
},
|
||||
"CUSTOM_ATTRIBUTES": {
|
||||
"TITLE": "Custom Attributes",
|
||||
"BUTTON": "Add custom attribute",
|
||||
|
|
|
@ -110,6 +110,7 @@
|
|||
"CONVERSATION_SIDEBAR": {
|
||||
"DETAILS_TITLE": "Conversations Details",
|
||||
"ASSIGNEE_LABEL": "Assigned Agent",
|
||||
"SELF_ASSIGN": "Assign to me",
|
||||
"TEAM_LABEL": "Assigned Team",
|
||||
"SELECT": {
|
||||
"PLACEHOLDER": "None"
|
||||
|
|
|
@ -28,6 +28,14 @@
|
|||
}
|
||||
],
|
||||
"ADD": {
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Inbox Name",
|
||||
"PLACEHOLDER": "Enter your inbox name (eg: Acme Inc)"
|
||||
},
|
||||
"WEBSITE_NAME": {
|
||||
"LABEL": "Website Name",
|
||||
"PLACEHOLDER": "Enter your website name (eg: Acme Inc)"
|
||||
},
|
||||
"FB": {
|
||||
"HELP": "PS: By signing in, we only get access to your Page's messages. Your private messages can never be accessed by Chatwoot.",
|
||||
"CHOOSE_PAGE": "Choose Page",
|
||||
|
@ -48,10 +56,6 @@
|
|||
"CHANNEL_AVATAR": {
|
||||
"LABEL": "Channel Avatar"
|
||||
},
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Website Name",
|
||||
"PLACEHOLDER": "Enter your website name (eg: Acme Inc)"
|
||||
},
|
||||
"CHANNEL_DOMAIN": {
|
||||
"LABEL": "Website Domain",
|
||||
"PLACEHOLDER": "Enter your website domain (eg: acme.com)"
|
||||
|
@ -208,6 +212,10 @@
|
|||
"AUTO_ASSIGNMENT": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"EMAIL_COLLECT_BOX": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
|
@ -215,6 +223,7 @@
|
|||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"PLACE_HOLDER": "Please type {inboxName} to confirm",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
},
|
||||
|
@ -243,6 +252,8 @@
|
|||
"INBOX_AGENTS": "Agents",
|
||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||
"UPDATE": "Update",
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
||||
"AUTO_ASSIGNMENT": "Enable auto assignment",
|
||||
"INBOX_UPDATE_TITLE": "Inbox Settings",
|
||||
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
|
||||
|
|
|
@ -15,6 +15,7 @@ import { default as _setNewPassword } from './setNewPassword.json';
|
|||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
import { default as _teamsSettings } from './teamsSettings.json';
|
||||
import { default as _integrationApps } from './integrationApps.json';
|
||||
|
||||
export default {
|
||||
..._agentMgmt,
|
||||
|
@ -34,4 +35,5 @@ export default {
|
|||
..._settings,
|
||||
..._signup,
|
||||
..._teamsSettings,
|
||||
..._integrationApps,
|
||||
};
|
||||
|
|
62
app/javascript/dashboard/i18n/locale/en/integrationApps.json
Normal file
62
app/javascript/dashboard/i18n/locale/en/integrationApps.json
Normal file
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"INTEGRATION_APPS": {
|
||||
"FETCHING": "Fetching Integrations",
|
||||
"NO_HOOK_CONFIGURED": "There are no %{integrationId} integrations configured in this account.",
|
||||
"HEADER": "Applications",
|
||||
"STATUS": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"CONFIGURE": "Configure",
|
||||
"ADD_BUTTON": "Add a new hook",
|
||||
"DELETE": {
|
||||
"TITLE": {
|
||||
"INBOX": "Confirm deletion",
|
||||
"ACCOUNT": "Disconnect"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"INBOX": "Are you sure to delete?",
|
||||
"ACCOUNT": "Are you sure to disconnect?"
|
||||
},
|
||||
"CONFIRM_BUTTON_TEXT": {
|
||||
"INBOX": "Yes, Delete",
|
||||
"ACCOUNT": "Yes, Disconnect"
|
||||
},
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Hook deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"FETCHING": "Fetching integration hooks",
|
||||
"INBOX": "Inbox",
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"FORM": {
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox"
|
||||
},
|
||||
"SUBMIT": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Integration hook added successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"CONNECT": {
|
||||
"BUTTON_TEXT": "Connect"
|
||||
},
|
||||
"DISCONNECT": {
|
||||
"BUTTON_TEXT": "Disconnect"
|
||||
},
|
||||
"SIDEBAR_DESCRIPTION": {
|
||||
"DIALOGFLOW": "Dialogflow is a natural language understanding platform that makes it easy to design and integrate a conversational user interface into your mobile app, web application, device, bot, interactive voice response system, and so on. <br /> <br /> Dialogflow integration with %{installationName} allows you to configure a Dialogflow bot with your inboxes which lets the bot handle the queries initially and hand them over to an agent when needed. Dialogflow can be used to qualifying the leads, reduce the workload of agents by providing frequently asked questions etc. <br /> <br /> To add Dialogflow, you need to create a Service Account in your Google project console and share the credentials. Please refer to the Dialogflow docs for more information."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,10 +12,25 @@
|
|||
"LIST": {
|
||||
"404": "There are no webhooks configured for this account.",
|
||||
"TITLE": "Manage webhooks",
|
||||
"TABLE_HEADER": [
|
||||
"Webhook endpoint",
|
||||
"Actions"
|
||||
]
|
||||
"TABLE_HEADER": ["Webhook endpoint", "Actions"]
|
||||
},
|
||||
"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",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"ADD": {
|
||||
"CANCEL": "Cancel",
|
||||
|
|
|
@ -9,17 +9,15 @@
|
|||
"404": "There are no labels available in this account.",
|
||||
"TITLE": "Manage labels",
|
||||
"DESC": "Labels let you group the conversations together.",
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Description",
|
||||
"Color"
|
||||
]
|
||||
"TABLE_HEADER": ["Name", "Description", "Color"]
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Label Name",
|
||||
"PLACEHOLDER": "Label name",
|
||||
"ERROR": "Label Name is required"
|
||||
"REQUIRED_ERROR": "Label name is required",
|
||||
"MINIMUM_LENGTH_ERROR": "Minimum length 2 is required",
|
||||
"VALID_ERROR": "Only Alphabets, Numbers, Hyphen and Underscore are allowed"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
|
|
|
@ -74,6 +74,11 @@
|
|||
"ERROR": "Please enter a valid email address",
|
||||
"PLACEHOLDER": "Please enter your email address, this would be displayed in conversations"
|
||||
},
|
||||
"CURRENT_PASSWORD": {
|
||||
"LABEL": "Current password",
|
||||
"ERROR": "Please enter the current password",
|
||||
"PLACEHOLDER": "Please enter the current password"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
"ERROR": "Please enter a password of length 6 or more",
|
||||
|
@ -128,6 +133,7 @@
|
|||
"CANNED_RESPONSES": "Canned Responses",
|
||||
"INTEGRATIONS": "Integrations",
|
||||
"ACCOUNT_SETTINGS": "Account Settings",
|
||||
"APPLICATIONS": "Applications",
|
||||
"LABELS": "Labels",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"DESC": "Add Agents to your newly created team. This lets you collaborate as a team on conversations, get notified on new events in the same conversation."
|
||||
},
|
||||
"WIZARD": [{
|
||||
"WIZARD": [
|
||||
{
|
||||
"title": "Create",
|
||||
"route": "settings_teams_new",
|
||||
"body": "Create a new team of agents."
|
||||
|
@ -45,7 +46,8 @@
|
|||
"TITLE": "Add agents to team - %{teamName}",
|
||||
"DESC": "Add Agents to your newly created team. All the added agents will be notified when a conversation is assigned to this team."
|
||||
},
|
||||
"WIZARD": [{
|
||||
"WIZARD": [
|
||||
{
|
||||
"title": "Team details",
|
||||
"route": "settings_teams_edit",
|
||||
"body": "Change name, description and other details."
|
||||
|
@ -97,6 +99,7 @@
|
|||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Are you sure want to delete - %{teamName}",
|
||||
"PLACE_HOLDER": "Please type {teamName} to confirm",
|
||||
"MESSAGE": "Deleting the team will remove the team assignment from the conversations assigned to this team.",
|
||||
"YES": "Delete ",
|
||||
"NO": "Cancel"
|
||||
|
|
|
@ -2,31 +2,33 @@
|
|||
<loading-state :message="$t('CONFIRM_EMAIL')"></loading-state>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
import LoadingState from '../../components/widgets/LoadingState';
|
||||
import Auth from '../../api/auth';
|
||||
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
||||
export default {
|
||||
props: {
|
||||
confirmationToken: String,
|
||||
redirectUrl: String,
|
||||
config: String,
|
||||
},
|
||||
components: {
|
||||
LoadingState,
|
||||
},
|
||||
props: {
|
||||
confirmationToken: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.confirmToken();
|
||||
},
|
||||
methods: {
|
||||
confirmToken() {
|
||||
Auth.verifyPasswordToken({
|
||||
confirmationToken: this.confirmationToken
|
||||
}).then(res => {
|
||||
window.location = res.data.redirect_url;
|
||||
}).catch(res => {
|
||||
window.location = '/';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
async confirmToken() {
|
||||
try {
|
||||
await Auth.verifyPasswordToken({
|
||||
confirmationToken: this.confirmationToken,
|
||||
});
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
} catch (error) {
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -46,12 +46,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import Auth from '../../api/auth';
|
||||
|
||||
import WootSubmitButton from '../../components/buttons/FormSubmitButton';
|
||||
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -81,7 +80,7 @@ export default {
|
|||
// If url opened without token
|
||||
// redirect to login
|
||||
if (!this.resetPasswordToken) {
|
||||
window.location = '/';
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
|
@ -118,11 +117,15 @@ export default {
|
|||
Auth.setNewPassword(credentials)
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
window.location = '/';
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.showAlert(this.$t('SET_NEW_PASSWORD.API.ERROR_MESSAGE'));
|
||||
.catch(error => {
|
||||
let errorMessage = this.$t('SET_NEW_PASSWORD.API.ERROR_MESSAGE');
|
||||
if (error?.data?.message) {
|
||||
errorMessage = error.data.message;
|
||||
}
|
||||
this.showAlert(errorMessage);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
<script>
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import Auth from '../../api/auth';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -71,7 +70,6 @@ export default {
|
|||
successMessage = res.data.message;
|
||||
}
|
||||
this.showAlert(successMessage);
|
||||
window.location = frontendURL('login');
|
||||
})
|
||||
.catch(error => {
|
||||
let errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
|
||||
|
|
|
@ -25,6 +25,17 @@
|
|||
"
|
||||
@blur="$v.credentials.fullName.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="credentials.email"
|
||||
type="email"
|
||||
:class="{ error: $v.credentials.email.$error }"
|
||||
:label="$t('REGISTER.EMAIL.LABEL')"
|
||||
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : ''
|
||||
"
|
||||
@blur="$v.credentials.email.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="credentials.accountName"
|
||||
:class="{ error: $v.credentials.accountName.$error }"
|
||||
|
@ -38,15 +49,31 @@
|
|||
@blur="$v.credentials.accountName.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="credentials.email"
|
||||
type="email"
|
||||
:class="{ error: $v.credentials.email.$error }"
|
||||
:label="$t('REGISTER.EMAIL.LABEL')"
|
||||
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
:class="{ error: $v.credentials.password.$error }"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : ''
|
||||
$v.credentials.password.$error
|
||||
? $t('SET_NEW_PASSWORD.PASSWORD.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.credentials.email.$touch"
|
||||
@blur="$v.credentials.password.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model.trim="credentials.confirmPassword"
|
||||
type="password"
|
||||
:class="{ error: $v.credentials.confirmPassword.$error }"
|
||||
:label="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.credentials.confirmPassword.$error
|
||||
? $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.credentials.confirmPassword.$touch"
|
||||
/>
|
||||
<woot-submit-button
|
||||
:disabled="isSignupInProgress"
|
||||
|
@ -79,6 +106,7 @@ import Auth from '../../api/auth';
|
|||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin, alertMixin],
|
||||
|
@ -88,6 +116,8 @@ export default {
|
|||
accountName: '',
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
isSignupInProgress: false,
|
||||
error: '',
|
||||
|
@ -107,6 +137,19 @@ export default {
|
|||
required,
|
||||
email,
|
||||
},
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(6),
|
||||
},
|
||||
confirmPassword: {
|
||||
required,
|
||||
isEqPassword(value) {
|
||||
if (value !== this.credentials.password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -132,7 +175,7 @@ export default {
|
|||
try {
|
||||
const response = await Auth.register(this.credentials);
|
||||
if (response.status === 200) {
|
||||
window.location = '/';
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
}
|
||||
} catch (error) {
|
||||
let errorMessage = this.$t('REGISTER.API.ERROR_MESSAGE');
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import AddReminder from './AddReminder';
|
||||
|
||||
export default {
|
||||
title: 'Components/Reminder/Add',
|
||||
component: AddReminder,
|
||||
argTypes: {
|
||||
options: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AddReminder },
|
||||
template:
|
||||
'<add-reminder v-bind="$props" @add="onAdd" @click="onClick"></add-reminder>',
|
||||
});
|
||||
|
||||
export const Add = Template.bind({});
|
||||
Add.args = {
|
||||
onAdd: action('Added'),
|
||||
onClick: action('Label'),
|
||||
options: [
|
||||
{
|
||||
id: '12345',
|
||||
name: 'calls',
|
||||
},
|
||||
{
|
||||
id: '12346',
|
||||
name: 'meeting',
|
||||
},
|
||||
{
|
||||
id: '12347',
|
||||
name: 'review',
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<div class="wrap">
|
||||
<div class="input-select-wrap">
|
||||
<textarea
|
||||
v-model="content"
|
||||
class="input--reminder"
|
||||
@keydown.enter.shift.exact="onAdd"
|
||||
>
|
||||
</textarea>
|
||||
<div class="select-wrap">
|
||||
<div class="select">
|
||||
<div class="input-group">
|
||||
<i class="ion-android-calendar input-group-label" />
|
||||
<input
|
||||
v-model="date"
|
||||
type="text"
|
||||
:placeholder="$t('REMINDER.FOOTER.DUE_DATE')"
|
||||
class="input-group-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="task-wrap">
|
||||
<select class="task__type" @change="optionSelected($event)">
|
||||
<option value="" disabled selected>
|
||||
{{ $t('REMINDER.FOOTER.LABEL_TITLE') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option.id"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
size="tiny"
|
||||
color-scheme="primary"
|
||||
class-names="add-button"
|
||||
:title="$t('REMINDER.ADD_BUTTON.TITLE')"
|
||||
:is-disabled="buttonDisabled"
|
||||
@click="onAdd"
|
||||
>
|
||||
{{ $t('REMINDER.ADD_BUTTON.BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
date: '',
|
||||
label: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
buttonDisabled() {
|
||||
return this.content && this.date === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetValue() {
|
||||
this.content = '';
|
||||
this.date = '';
|
||||
},
|
||||
|
||||
optionSelected(event) {
|
||||
this.label = event.target.value;
|
||||
},
|
||||
|
||||
onAdd() {
|
||||
const task = {
|
||||
content: this.content,
|
||||
date: this.date,
|
||||
label: this.label,
|
||||
};
|
||||
this.$emit('add', task);
|
||||
this.resetValue();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
display: flex;
|
||||
margin-bottom: var(--space-smaller);
|
||||
width: 100%;
|
||||
|
||||
.input-select-wrap {
|
||||
padding: var(--space-small) var(--space-small);
|
||||
width: 100%;
|
||||
|
||||
.input--reminder {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-bottom: var(--space-small);
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.select-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 0;
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
.input-group-field {
|
||||
height: var(--space-medium);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
}
|
||||
|
||||
.task-wrap {
|
||||
.task__type {
|
||||
margin: 0 0 0 var(--space-smaller);
|
||||
height: var(--space-medium);
|
||||
width: fit-content;
|
||||
padding: 0 var(--space-two) 0 var(--space-smaller);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -24,7 +24,7 @@ export default {
|
|||
},
|
||||
},
|
||||
timeStamp: {
|
||||
defaultValue: '1618046084',
|
||||
defaultValue: 1618046084,
|
||||
control: {
|
||||
type: 'number',
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="footer">
|
||||
<div class="meta">
|
||||
<div :title="userName">
|
||||
<Thumbnail :src="thumbnail" :username="userName" size="16px" />
|
||||
<thumbnail :src="thumbnail" :username="userName" size="16px" />
|
||||
</div>
|
||||
<div class="date-wrap">
|
||||
<span>{{ readableTime }}</span>
|
||||
|
@ -14,19 +14,17 @@
|
|||
</div>
|
||||
<div class="actions">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="ion-compose"
|
||||
color-scheme="secondary"
|
||||
class-names="button--emoji"
|
||||
@click="onEdit"
|
||||
/>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="ion-trash-b"
|
||||
color-scheme="secondary"
|
||||
class-names="button--emoji"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
|
@ -35,14 +33,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
WootButton,
|
||||
},
|
||||
|
||||
mixins: [timeMixin],
|
||||
|
@ -90,12 +86,13 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
.note__content {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-bottom: var(--space-small);
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
|
@ -111,12 +108,10 @@ export default {
|
|||
.actions {
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.button--emoji {
|
||||
margin-left: var(--space-small);
|
||||
height: var(--space-medium);
|
||||
width: var(--space-medium);
|
||||
.button {
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import ReminderItem from './ReminderItem';
|
||||
|
||||
export default {
|
||||
title: 'Components/Reminder/Item',
|
||||
component: ReminderItem,
|
||||
argTypes: {
|
||||
id: {
|
||||
control: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
text: {
|
||||
defaultValue:
|
||||
'A copy and paste musical notes symbols & music symbols collection for easy access.',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
isCompleted: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
date: {
|
||||
defaultValue: '03/06/2020',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
defaultValue: 'Call',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { ReminderItem },
|
||||
template:
|
||||
'<reminder-item v-bind="$props" @completed="onClick" @edit="onEdit" @delete="onDelete"></reminder-item>',
|
||||
});
|
||||
|
||||
export const Item = Template.bind({});
|
||||
Item.args = {
|
||||
onClick: action('Marked'),
|
||||
onEdit: action('Edit'),
|
||||
onDelete: action('Delete'),
|
||||
};
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div class="reminder-wrap">
|
||||
<div class="status-wrap">
|
||||
<input :checked="isCompleted" type="radio" @click="onClick" />
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<p class="content">
|
||||
{{ text }}
|
||||
</p>
|
||||
<div class="footer">
|
||||
<div class="meta">
|
||||
<woot-label
|
||||
:title="date"
|
||||
description="date"
|
||||
icon="ion-android-calendar"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
<woot-label
|
||||
:title="label"
|
||||
description="label"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
icon="ion-compose"
|
||||
color-scheme="secondary"
|
||||
class="action-button"
|
||||
@click="onEdit"
|
||||
/>
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
icon="ion-trash-b"
|
||||
color-scheme="secondary"
|
||||
class="action-button"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isCompleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('completed', this.isCompleted);
|
||||
},
|
||||
onEdit() {
|
||||
this.$emit('edit', this.id);
|
||||
},
|
||||
onDelete() {
|
||||
this.$emit('delete', this.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reminder-wrap {
|
||||
display: flex;
|
||||
margin-bottom: var(--space-smaller);
|
||||
|
||||
.status-wrap {
|
||||
padding: var(--space-small) var(--space-smaller);
|
||||
margin-top: var(--space-smaller);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
padding: var(--space-small);
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
|
||||
.action-button {
|
||||
margin-right: var(--space-small);
|
||||
height: var(--space-medium);
|
||||
width: var(--space-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.reminder-wrap:hover {
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import SectionHeader from './SectionHeader';
|
||||
|
||||
export default {
|
||||
title: 'Components/Events/Section',
|
||||
component: SectionHeader,
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { SectionHeader },
|
||||
template:
|
||||
'<section-header v-bind="$props" @notes="onClickNotes" @events="onClickEvents" @conversation="onClickConversation"></section-header>',
|
||||
});
|
||||
|
||||
export const Section = Template.bind({});
|
||||
Section.args = {
|
||||
onClickNotes: action('notes'),
|
||||
onClickEvents: action('events'),
|
||||
onClickConversation: action('conversation'),
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div class="wrap">
|
||||
<div class="header">
|
||||
<h5 class="block-title">
|
||||
{{ $t('EVENTS.HEADER.TITLE') }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="button-wrap">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="pill-button"
|
||||
@click="onClickNotes"
|
||||
>
|
||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_NOTES') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="pill-button"
|
||||
@click="onClickEvents"
|
||||
>
|
||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_EVENTS') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class-names="pill-button"
|
||||
@click="onClickConversation"
|
||||
>
|
||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_CONVO') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
onClickNotes() {
|
||||
this.$emit('notes');
|
||||
},
|
||||
|
||||
onClickEvents() {
|
||||
this.$emit('events');
|
||||
},
|
||||
|
||||
onClickConversation() {
|
||||
this.$emit('conversation');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
width: 100%;
|
||||
padding: var(--space-normal);
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
|
||||
.pill-button {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import TimelineCard from './TimelineCard';
|
||||
|
||||
export default {
|
||||
title: 'Components/Events/Timeline',
|
||||
component: TimelineCard,
|
||||
argTypes: {
|
||||
eventType: {
|
||||
defaultValue: 'Commented',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
eventPath: {
|
||||
defaultValue: 'chatwoot/chatwoot',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
eventBody: {
|
||||
defaultValue:
|
||||
'Commentedmany variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
timeStamp: {
|
||||
defaultValue: '1618046084',
|
||||
control: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { TimelineCard },
|
||||
template: '<timeline-card v-bind="$props" @more="onClick"></timeline-card>',
|
||||
});
|
||||
|
||||
export const Timeline = Template.bind({});
|
||||
Timeline.args = {
|
||||
onClick: action('more'),
|
||||
};
|
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div class="timeline-card-wrap">
|
||||
<div class="icon-chatbox">
|
||||
<i class="ion-chatboxes" />
|
||||
</div>
|
||||
<div class="card-wrap">
|
||||
<div class="header">
|
||||
<div class="text-wrap">
|
||||
<h6 class="text-block-title">
|
||||
{{ eventType }}
|
||||
</h6>
|
||||
<span class="event-path">on {{ eventPath }}</span>
|
||||
</div>
|
||||
<div class="date-wrap">
|
||||
<span>{{ readableTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-wrap">
|
||||
<p class="comment">
|
||||
{{ eventBody }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-more" @click="onClick">
|
||||
<i class="ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
export default {
|
||||
mixins: [timeMixin],
|
||||
|
||||
props: {
|
||||
eventType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
eventPath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
eventBody: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
timeStamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
readableTime() {
|
||||
return this.dynamicTime(this.timeStamp);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('more');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timeline-card-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: var(--color-body);
|
||||
padding: var(--space-small);
|
||||
|
||||
.icon-chatbox {
|
||||
width: var(--space-large);
|
||||
height: var(--space-large);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: var(--color-background);
|
||||
|
||||
.ion-chatboxes {
|
||||
font-size: var(--font-size-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.card-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: var(--space-smaller) var(--space-normal) 0;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.text-wrap {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.event-path {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.date-wrap {
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-wrap {
|
||||
border: 1px solid var(--color-border-light);
|
||||
|
||||
.comment {
|
||||
padding: var(--space-small);
|
||||
font-size: var(--font-size-mini);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-more {
|
||||
.ion-android-more-vertical {
|
||||
font-size: var(--font-size-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<contact-details-item
|
||||
:title="$t('CONTACT_PANEL.CUSTOM_ATTRIBUTES.TITLE')"
|
||||
:title="$t('CUSTOM_ATTRIBUTES.TITLE')"
|
||||
icon="ion-code"
|
||||
emoji="📕"
|
||||
/>
|
||||
|
|
|
@ -9,9 +9,20 @@
|
|||
{{ $t('CONVERSATION_SIDEBAR.DETAILS_TITLE') }}
|
||||
</h4>
|
||||
<div class="multiselect-wrap--small">
|
||||
<label class="multiselect__label">
|
||||
{{ $t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL') }}
|
||||
</label>
|
||||
<div class="self-assign">
|
||||
<label class="multiselect__label">
|
||||
{{ $t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL') }}
|
||||
</label>
|
||||
<woot-button
|
||||
v-if="showSelfAssign"
|
||||
icon="ion-arrow-right-c"
|
||||
variant="link"
|
||||
class-names="button-content"
|
||||
@click="onSelfAssign"
|
||||
>
|
||||
{{ $t('CONVERSATION_SIDEBAR.SELF_ASSIGN') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-on-clickaway="onCloseDropdown" class="dropdown-wrap">
|
||||
<button
|
||||
:v-model="assignedAgent"
|
||||
|
@ -221,6 +232,7 @@ export default {
|
|||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
teams: 'teams/getTeams',
|
||||
currentUser: 'getCurrentUser',
|
||||
getAgents: 'inboxAssignableAgents/getAssignableAgents',
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
}),
|
||||
|
@ -326,6 +338,15 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
showSelfAssign() {
|
||||
if (!this.assignedAgent) {
|
||||
return true;
|
||||
}
|
||||
if (this.assignedAgent.id !== this.currentUser.id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
conversationId(newConversationId, prevConversationId) {
|
||||
|
@ -380,6 +401,29 @@ export default {
|
|||
onCloseDropdownTeam() {
|
||||
this.showSearchDropdownTeam = false;
|
||||
},
|
||||
onSelfAssign() {
|
||||
const {
|
||||
account_id,
|
||||
availability_status,
|
||||
available_name,
|
||||
email,
|
||||
id,
|
||||
name,
|
||||
role,
|
||||
thumbnail,
|
||||
} = this.currentUser;
|
||||
const selfAssign = {
|
||||
account_id,
|
||||
availability_status,
|
||||
available_name,
|
||||
email,
|
||||
id,
|
||||
name,
|
||||
role,
|
||||
thumbnail,
|
||||
};
|
||||
this.assignedAgent = selfAssign;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -534,4 +578,12 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
.self-assign {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.button-content {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="
|
||||
$t('CONTACT_PANEL.LABELS.MODAL.TITLE') + ' #' + conversationId
|
||||
"
|
||||
/>
|
||||
<div class="content">
|
||||
<div class="label-content--block">
|
||||
<div class="label-content--title">
|
||||
{{ $t('CONTACT_PANEL.LABELS.MODAL.ACTIVE_LABELS') }}
|
||||
<span v-tooltip.bottom="$t('CONTACT_PANEL.LABELS.MODAL.REMOVE')">
|
||||
<i class="ion-ios-help-outline" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="activeList.length">
|
||||
<woot-label
|
||||
v-for="label in activeList"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:bg-color="label.color"
|
||||
:show-close="true"
|
||||
@click="onRemove"
|
||||
/>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ $t('CONTACT_PANEL.LABELS.NO_AVAILABLE_LABELS') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="label-content--block">
|
||||
<div class="label-content--title">
|
||||
{{ $t('CONTACT_PANEL.LABELS.MODAL.INACTIVE_LABELS') }}
|
||||
<span v-tooltip.bottom="$t('CONTACT_PANEL.LABELS.MODAL.ADD')">
|
||||
<i class="ion-ios-help-outline" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="inactiveList.length">
|
||||
<woot-label
|
||||
v-for="label in inactiveList"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:bg-color="label.color"
|
||||
icon="ion-plus"
|
||||
@click="onAdd"
|
||||
/>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ $t('CONTACT_PANEL.LABELS.NO_LABELS_TO_ADD') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
accountLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
savedLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
updateLabels: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
activeList() {
|
||||
return this.accountLabels
|
||||
.filter(accountLabel => this.savedLabels.includes(accountLabel.title))
|
||||
.sort((a, b) => {
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
},
|
||||
inactiveList() {
|
||||
return this.accountLabels
|
||||
.filter(accountLabel => !this.savedLabels.includes(accountLabel.title))
|
||||
.sort((a, b) => {
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onAdd(label) {
|
||||
const activeLabels = this.activeList.map(
|
||||
activeLabel => activeLabel.title
|
||||
);
|
||||
this.updateLabels([...activeLabels, label]);
|
||||
},
|
||||
|
||||
onRemove(label) {
|
||||
const activeLabels = this.activeList
|
||||
.filter(activeLabel => activeLabel.title !== label)
|
||||
.map(activeLabel => activeLabel.title);
|
||||
this.updateLabels(activeLabels);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.label-content--block {
|
||||
margin-bottom: $space-normal;
|
||||
}
|
||||
|
||||
.label-content--title {
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: $space-normal;
|
||||
}
|
||||
</style>
|
|
@ -49,7 +49,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
/* eslint no-console: 0 */
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
|
||||
|
|
|
@ -29,16 +29,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
channelList: [
|
||||
{ key: 'website', name: 'Website' },
|
||||
{ key: 'facebook', name: 'Facebook' },
|
||||
{ key: 'twitter', name: 'Twitter' },
|
||||
{ key: 'twilio', name: 'Twilio' },
|
||||
{ key: 'email', name: 'Email' },
|
||||
{ key: 'api', name: 'API' },
|
||||
{ key: 'telegram', name: 'Telegram' },
|
||||
{ key: 'line', name: 'Line' },
|
||||
],
|
||||
enabledFeatures: {},
|
||||
};
|
||||
},
|
||||
|
@ -46,8 +36,26 @@ export default {
|
|||
account() {
|
||||
return this.$store.getters['accounts/getAccount'](this.accountId);
|
||||
},
|
||||
channelList() {
|
||||
const { apiChannelName, apiChannelThumbnail } = this.globalConfig;
|
||||
return [
|
||||
{ key: 'website', name: 'Website' },
|
||||
{ key: 'facebook', name: 'Facebook' },
|
||||
{ key: 'twitter', name: 'Twitter' },
|
||||
{ key: 'twilio', name: 'Twilio' },
|
||||
{ key: 'email', name: 'Email' },
|
||||
{
|
||||
key: 'api',
|
||||
name: apiChannelName || 'API',
|
||||
thumbnail: apiChannelThumbnail,
|
||||
},
|
||||
{ key: 'telegram', name: 'Telegram' },
|
||||
{ key: 'line', name: 'Line' },
|
||||
];
|
||||
},
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
Email
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Api'">
|
||||
Api
|
||||
{{ globalConfig.apiChannelName || 'API' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
@ -106,14 +106,16 @@
|
|||
:inbox="selectedInbox"
|
||||
/>
|
||||
|
||||
<woot-delete-modal
|
||||
<woot-confirm-delete-modal
|
||||
:show.sync="showDeletePopup"
|
||||
:on-close="closeDelete"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('INBOX_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="deleteMessage"
|
||||
:message="confirmDeleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
:confirm-value="selectedInbox.name"
|
||||
:confirm-place-holder-text="confirmPlaceHolderText"
|
||||
@on-confirm="confirmDeletion"
|
||||
@on-close="closeDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -153,11 +155,16 @@ export default {
|
|||
this.selectedInbox.name
|
||||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
confirmDeleteMessage() {
|
||||
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
|
||||
this.selectedInbox.name
|
||||
} ?`;
|
||||
},
|
||||
confirmPlaceHolderText() {
|
||||
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.PLACE_HOLDER', {
|
||||
inboxName: this.selectedInbox.name,
|
||||
})}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openSettings(inbox) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="settings columns container">
|
||||
<woot-modal-header
|
||||
<setting-intro-banner
|
||||
:header-image="inbox.avatarUrl"
|
||||
:header-title="inboxName"
|
||||
>
|
||||
|
@ -12,7 +12,7 @@
|
|||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</woot-modal-header>
|
||||
</setting-intro-banner>
|
||||
|
||||
<div v-if="selectedTabKey === 'inbox_settings'" class="settings--content">
|
||||
<settings-section
|
||||
|
@ -25,13 +25,10 @@
|
|||
@change="handleImageUpload"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model.trim="selectedInboxName"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL')"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
|
||||
"
|
||||
:label="inboxNameLabel"
|
||||
:placeholder="inboxNamePlaceHolder"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAWebWidgetInbox"
|
||||
|
@ -141,6 +138,23 @@
|
|||
</p>
|
||||
</label>
|
||||
|
||||
<label v-if="isAWebWidgetInbox" class="medium-9 columns">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.ENABLE_EMAIL_COLLECT_BOX') }}
|
||||
<select v-model="emailCollectEnabled">
|
||||
<option :value="true">
|
||||
{{ $t('INBOX_MGMT.EDIT.EMAIL_COLLECT_BOX.ENABLED') }}
|
||||
</option>
|
||||
<option :value="false">
|
||||
{{ $t('INBOX_MGMT.EDIT.EMAIL_COLLECT_BOX.DISABLED') }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="help-text">
|
||||
{{
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT')
|
||||
}}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label class="medium-9 columns">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT') }}
|
||||
<select v-model="autoAssignment">
|
||||
|
@ -268,6 +282,7 @@ import { mapGetters } from 'vuex';
|
|||
import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner';
|
||||
import SettingsSection from '../../../../components/SettingsSection';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import FacebookReauthorize from './facebook/Reauthorize';
|
||||
|
@ -277,6 +292,7 @@ import Campaign from './components/Campaign';
|
|||
|
||||
export default {
|
||||
components: {
|
||||
SettingIntroBanner,
|
||||
SettingsSection,
|
||||
FacebookReauthorize,
|
||||
PreChatFormSettings,
|
||||
|
@ -292,6 +308,7 @@ export default {
|
|||
greetingEnabled: true,
|
||||
greetingMessage: '',
|
||||
autoAssignment: false,
|
||||
emailCollectEnabled: false,
|
||||
isAgentListUpdating: false,
|
||||
selectedInboxName: '',
|
||||
channelWebsiteUrl: '',
|
||||
|
@ -381,6 +398,18 @@ export default {
|
|||
messengerScript() {
|
||||
return createMessengerScript(this.inbox.page_id);
|
||||
},
|
||||
inboxNameLabel() {
|
||||
if (this.isAWebWidgetInbox) {
|
||||
return this.$t('INBOX_MGMT.ADD.WEBSITE_NAME.LABEL');
|
||||
}
|
||||
return this.$t('INBOX_MGMT.ADD.CHANNEL_NAME.LABEL');
|
||||
},
|
||||
inboxNamePlaceHolder() {
|
||||
if (this.isAWebWidgetInbox) {
|
||||
return this.$t('INBOX_MGMT.ADD.WEBSITE_NAME.PLACEHOLDER');
|
||||
}
|
||||
return this.$t('INBOX_MGMT.ADD.CHANNEL_NAME.PLACEHOLDER');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to) {
|
||||
|
@ -421,6 +450,7 @@ export default {
|
|||
this.greetingEnabled = this.inbox.greeting_enabled;
|
||||
this.greetingMessage = this.inbox.greeting_message;
|
||||
this.autoAssignment = this.inbox.enable_auto_assignment;
|
||||
this.emailCollectEnabled = this.inbox.enable_email_collect;
|
||||
this.channelWebsiteUrl = this.inbox.website_url;
|
||||
this.channelWelcomeTitle = this.inbox.welcome_title;
|
||||
this.channelWelcomeTagline = this.inbox.welcome_tagline;
|
||||
|
@ -461,6 +491,7 @@ export default {
|
|||
id: this.currentInboxId,
|
||||
name: this.selectedInboxName,
|
||||
enable_auto_assignment: this.autoAssignment,
|
||||
enable_email_collect: this.emailCollectEnabled,
|
||||
greeting_enabled: this.greetingEnabled,
|
||||
greeting_message: this.greetingMessage || '',
|
||||
channel: {
|
||||
|
@ -509,15 +540,9 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.page-top-bar {
|
||||
@include background-light;
|
||||
@include border-normal-bottom;
|
||||
padding: $space-normal $space-large 0;
|
||||
|
||||
.tabs {
|
||||
padding: 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.tabs {
|
||||
padding: 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,13 +15,11 @@
|
|||
>
|
||||
<div class="medium-12 columns">
|
||||
<label>
|
||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL') }}
|
||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="inboxName"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
|
||||
"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WEBSITE_NAME.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -207,8 +207,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content-box .page-top-bar::v-deep {
|
||||
padding: var(--space-large) var(--space-large) var(--space-zero);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -239,8 +239,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content-box .page-top-bar::v-deep {
|
||||
padding: var(--space-large) var(--space-large) var(--space-zero);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<div class="row">
|
||||
<div class="empty-wrapper">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('INTEGRATION_APPS.FETCHING')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="small-12 columns integrations-wrap">
|
||||
<div class="row integrations">
|
||||
<div
|
||||
v-for="item in integrationsList"
|
||||
:key="item.id"
|
||||
class="small-12 columns integration"
|
||||
>
|
||||
<integration-item
|
||||
:integration-id="item.id"
|
||||
:integration-logo="item.logo"
|
||||
:integration-name="item.name"
|
||||
:integration-description="item.description"
|
||||
:integration-enabled="item.hooks.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import IntegrationItem from './IntegrationItem';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IntegrationItem,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'labels/getUIFlags',
|
||||
integrationsList: 'integrations/getAppIntegrations',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('integrations/get');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
.empty-wrapper {
|
||||
margin: var(--space-zero) auto;
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue